@cartanova/qgrid-ai-sdk 0.1.0 → 2.0.2
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/dist/index.d.ts +3 -2
- package/dist/index.js +17 -15
- 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/logger.test.ts
DELETED
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
import { type TelemetryIntegration } from "ai";
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { createQgridLogger } from "./logger";
|
|
5
|
-
|
|
6
|
-
const SERVER = "http://localhost:44900";
|
|
7
|
-
|
|
8
|
-
function getIntegration(config: Parameters<typeof createQgridLogger>[0]): TelemetryIntegration {
|
|
9
|
-
const settings = createQgridLogger(config);
|
|
10
|
-
const integrations = Array.isArray(settings.integrations)
|
|
11
|
-
? settings.integrations
|
|
12
|
-
: settings.integrations
|
|
13
|
-
? [settings.integrations]
|
|
14
|
-
: [];
|
|
15
|
-
return integrations[0]!;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type FetchCall = { url: string; body: { input: Record<string, unknown> } };
|
|
19
|
-
|
|
20
|
-
function mockFetch() {
|
|
21
|
-
const calls: FetchCall[] = [];
|
|
22
|
-
vi.stubGlobal(
|
|
23
|
-
"fetch",
|
|
24
|
-
vi.fn(async (url: string, init?: RequestInit) => {
|
|
25
|
-
const body = JSON.parse(String(init?.body)) as FetchCall["body"];
|
|
26
|
-
calls.push({ url, body });
|
|
27
|
-
|
|
28
|
-
if (url.includes("/createRun")) {
|
|
29
|
-
return new Response(JSON.stringify({ requestLogId: 1 }), {
|
|
30
|
-
status: 200,
|
|
31
|
-
headers: { "content-type": "application/json" },
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
return new Response(JSON.stringify({ stepId: 1, ok: true }), {
|
|
35
|
-
status: 200,
|
|
36
|
-
headers: { "content-type": "application/json" },
|
|
37
|
-
});
|
|
38
|
-
}),
|
|
39
|
-
);
|
|
40
|
-
return calls;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
vi.useRealTimers();
|
|
45
|
-
vi.unstubAllGlobals();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe("createQgridLogger", () => {
|
|
49
|
-
it("logs simple text generation (no tools)", async () => {
|
|
50
|
-
const calls = mockFetch();
|
|
51
|
-
const logger = getIntegration({ serverUrl: SERVER, projectName: "test" });
|
|
52
|
-
|
|
53
|
-
await logger.onStart!({
|
|
54
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
55
|
-
prompt: "Hello",
|
|
56
|
-
system: "You are helpful",
|
|
57
|
-
} as never);
|
|
58
|
-
|
|
59
|
-
await logger.onStepFinish!({
|
|
60
|
-
stepNumber: 0,
|
|
61
|
-
finishReason: "stop",
|
|
62
|
-
usage: { inputTokens: 100, outputTokens: 50, inputTokenDetails: {} },
|
|
63
|
-
content: [],
|
|
64
|
-
text: "Hi there",
|
|
65
|
-
} as never);
|
|
66
|
-
|
|
67
|
-
await logger.onFinish!({
|
|
68
|
-
finishReason: "stop",
|
|
69
|
-
text: "Hi there",
|
|
70
|
-
totalUsage: { inputTokens: 100, outputTokens: 50, inputTokenDetails: {} },
|
|
71
|
-
} as never);
|
|
72
|
-
|
|
73
|
-
const createRunCall = calls.find((c) => c.url.includes("/createRun"));
|
|
74
|
-
expect(createRunCall?.body.input).toMatchObject({
|
|
75
|
-
userPrompt: "Hello",
|
|
76
|
-
systemPrompt: "You are helpful",
|
|
77
|
-
modelName: "gemini-3-flash",
|
|
78
|
-
projectName: "test",
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const appendCalls = calls.filter((c) => c.url.includes("/appendStep"));
|
|
82
|
-
expect(appendCalls).toHaveLength(1);
|
|
83
|
-
expect(appendCalls[0].body.input).toMatchObject({
|
|
84
|
-
stepIndex: 0,
|
|
85
|
-
type: "generate",
|
|
86
|
-
finishReason: "stop",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
90
|
-
expect(finishCall?.body.input).toMatchObject({
|
|
91
|
-
requestLogId: 1,
|
|
92
|
-
status: "succeeded",
|
|
93
|
-
response: "Hi there",
|
|
94
|
-
tokenName: "external",
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("logs tool calling multi-step", async () => {
|
|
99
|
-
const calls = mockFetch();
|
|
100
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
101
|
-
|
|
102
|
-
await logger.onStart!({
|
|
103
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
104
|
-
prompt: "Weather in Seoul",
|
|
105
|
-
} as never);
|
|
106
|
-
|
|
107
|
-
// tool call finish — durationMs 캐시
|
|
108
|
-
await logger.onToolCallFinish!({
|
|
109
|
-
toolCall: { toolCallId: "call_1", toolName: "getWeather" },
|
|
110
|
-
durationMs: 150,
|
|
111
|
-
} as never);
|
|
112
|
-
|
|
113
|
-
// step 0: tool-calls (tool-call만 있고 tool-result는 다음 step에)
|
|
114
|
-
await logger.onStepFinish!({
|
|
115
|
-
stepNumber: 0,
|
|
116
|
-
finishReason: "tool-calls",
|
|
117
|
-
usage: { inputTokens: 200, outputTokens: 30, inputTokenDetails: {} },
|
|
118
|
-
content: [
|
|
119
|
-
{
|
|
120
|
-
type: "tool-call",
|
|
121
|
-
toolCallId: "call_1",
|
|
122
|
-
toolName: "getWeather",
|
|
123
|
-
input: { city: "Seoul" },
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
} as never);
|
|
127
|
-
|
|
128
|
-
// step 1: stop (이전 step의 tool-result가 여기 content에)
|
|
129
|
-
await logger.onStepFinish!({
|
|
130
|
-
stepNumber: 1,
|
|
131
|
-
finishReason: "stop",
|
|
132
|
-
usage: { inputTokens: 400, outputTokens: 80, inputTokenDetails: {} },
|
|
133
|
-
content: [{ type: "tool-result", toolCallId: "call_1", output: { temperature: 22 } }],
|
|
134
|
-
} as never);
|
|
135
|
-
|
|
136
|
-
await logger.onFinish!({
|
|
137
|
-
finishReason: "stop",
|
|
138
|
-
text: "Seoul is 22°C",
|
|
139
|
-
totalUsage: { inputTokens: 600, outputTokens: 110, inputTokenDetails: {} },
|
|
140
|
-
} as never);
|
|
141
|
-
|
|
142
|
-
const appendCalls = calls.filter((c) => c.url.includes("/appendStep"));
|
|
143
|
-
expect(appendCalls).toHaveLength(3); // generate + tool_call + generate
|
|
144
|
-
|
|
145
|
-
const toolCallStep = appendCalls.find((c) => c.body.input.type === "tool_call");
|
|
146
|
-
expect(toolCallStep?.body.input).toMatchObject({
|
|
147
|
-
toolName: "getWeather",
|
|
148
|
-
toolArgs: '{"city":"Seoul"}',
|
|
149
|
-
toolResult: '{"temperature":22}',
|
|
150
|
-
toolDurationMs: 150,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
154
|
-
expect(finishCall?.body.input).toMatchObject({
|
|
155
|
-
status: "succeeded",
|
|
156
|
-
totalInputTokens: 600,
|
|
157
|
-
totalOutputTokens: 110,
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("logs step reasoning text and token usage when provided", async () => {
|
|
162
|
-
const calls = mockFetch();
|
|
163
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
164
|
-
|
|
165
|
-
await logger.onStart!({
|
|
166
|
-
model: { provider: "openai", modelId: "gpt-5.4" },
|
|
167
|
-
prompt: "Think briefly",
|
|
168
|
-
} as never);
|
|
169
|
-
|
|
170
|
-
await logger.onStepFinish!({
|
|
171
|
-
stepNumber: 0,
|
|
172
|
-
finishReason: "stop",
|
|
173
|
-
usage: {
|
|
174
|
-
inputTokens: 10,
|
|
175
|
-
outputTokens: 20,
|
|
176
|
-
inputTokenDetails: {},
|
|
177
|
-
outputTokenDetails: { reasoningTokens: 7 },
|
|
178
|
-
},
|
|
179
|
-
reasoningText: "Need a concise answer.",
|
|
180
|
-
content: [],
|
|
181
|
-
} as never);
|
|
182
|
-
|
|
183
|
-
await logger.onFinish!({
|
|
184
|
-
finishReason: "stop",
|
|
185
|
-
text: "ok",
|
|
186
|
-
totalUsage: { inputTokens: 10, outputTokens: 20, inputTokenDetails: {} },
|
|
187
|
-
} as never);
|
|
188
|
-
|
|
189
|
-
const generateStep = calls.find(
|
|
190
|
-
(c) => c.url.includes("/appendStep") && c.body.input.type === "generate",
|
|
191
|
-
);
|
|
192
|
-
expect(generateStep?.body.input).toMatchObject({
|
|
193
|
-
reasoningText: "Need a concise answer.",
|
|
194
|
-
reasoningTokens: 7,
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("handles error — finishRun with status error", async () => {
|
|
199
|
-
const calls = mockFetch();
|
|
200
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
201
|
-
|
|
202
|
-
await logger.onStart!({
|
|
203
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
204
|
-
prompt: "test",
|
|
205
|
-
} as never);
|
|
206
|
-
|
|
207
|
-
await logger.onFinish!({
|
|
208
|
-
finishReason: "error",
|
|
209
|
-
text: "",
|
|
210
|
-
error: new Error("LLM failed"),
|
|
211
|
-
totalUsage: { inputTokens: 50, outputTokens: 0, inputTokenDetails: {} },
|
|
212
|
-
} as never);
|
|
213
|
-
|
|
214
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
215
|
-
expect(finishCall?.body.input).toMatchObject({
|
|
216
|
-
status: "error",
|
|
217
|
-
errorMessage: "Error: LLM failed",
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("skips when model.provider is qgrid", async () => {
|
|
222
|
-
const calls = mockFetch();
|
|
223
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
224
|
-
|
|
225
|
-
await logger.onStart!({
|
|
226
|
-
model: { provider: "qgrid", modelId: "openai/gpt-5.4" },
|
|
227
|
-
prompt: "test",
|
|
228
|
-
} as never);
|
|
229
|
-
|
|
230
|
-
await logger.onStepFinish!({
|
|
231
|
-
stepNumber: 0,
|
|
232
|
-
finishReason: "stop",
|
|
233
|
-
usage: { inputTokens: 100, outputTokens: 50 },
|
|
234
|
-
content: [],
|
|
235
|
-
} as never);
|
|
236
|
-
|
|
237
|
-
await logger.onFinish!({
|
|
238
|
-
finishReason: "stop",
|
|
239
|
-
text: "response",
|
|
240
|
-
totalUsage: { inputTokens: 100, outputTokens: 50 },
|
|
241
|
-
} as never);
|
|
242
|
-
|
|
243
|
-
expect(calls).toHaveLength(0);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("skips all hooks when createRun fails", async () => {
|
|
247
|
-
const errors: Error[] = [];
|
|
248
|
-
vi.stubGlobal(
|
|
249
|
-
"fetch",
|
|
250
|
-
vi.fn(async () => {
|
|
251
|
-
throw new Error("server down");
|
|
252
|
-
}),
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
const logger = getIntegration({
|
|
256
|
-
serverUrl: SERVER,
|
|
257
|
-
onLogError: (err) => errors.push(err),
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
await logger.onStart!({
|
|
261
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
262
|
-
prompt: "test",
|
|
263
|
-
} as never);
|
|
264
|
-
|
|
265
|
-
await logger.onStepFinish!({
|
|
266
|
-
stepNumber: 0,
|
|
267
|
-
finishReason: "stop",
|
|
268
|
-
usage: { inputTokens: 100, outputTokens: 50 },
|
|
269
|
-
content: [],
|
|
270
|
-
} as never);
|
|
271
|
-
|
|
272
|
-
await logger.onFinish!({
|
|
273
|
-
finishReason: "stop",
|
|
274
|
-
text: "response",
|
|
275
|
-
totalUsage: { inputTokens: 100, outputTokens: 50 },
|
|
276
|
-
} as never);
|
|
277
|
-
|
|
278
|
-
expect(errors).toHaveLength(1);
|
|
279
|
-
expect(errors[0].message).toBe("server down");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("uses messages array for prompt extraction", async () => {
|
|
283
|
-
const calls = mockFetch();
|
|
284
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
285
|
-
const messages = [
|
|
286
|
-
{ role: "user", content: [{ type: "text", text: "first message" }] },
|
|
287
|
-
{ role: "assistant", content: [{ type: "text", text: "response" }] },
|
|
288
|
-
{ role: "user", content: [{ type: "text", text: "second message" }] },
|
|
289
|
-
];
|
|
290
|
-
|
|
291
|
-
await logger.onStart!({
|
|
292
|
-
model: { provider: "anthropic", modelId: "claude-sonnet-4.7" },
|
|
293
|
-
prompt: undefined,
|
|
294
|
-
messages,
|
|
295
|
-
} as never);
|
|
296
|
-
|
|
297
|
-
await logger.onFinish!({
|
|
298
|
-
finishReason: "stop",
|
|
299
|
-
text: "done",
|
|
300
|
-
totalUsage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: {} },
|
|
301
|
-
} as never);
|
|
302
|
-
|
|
303
|
-
const createRunCall = calls.find((c) => c.url.includes("/createRun"));
|
|
304
|
-
expect(createRunCall?.body.input.userPrompt).toBe("second message");
|
|
305
|
-
|
|
306
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
307
|
-
// 마지막 user 메시지는 현재 turn이라 history에서 제외됨
|
|
308
|
-
expect(JSON.parse(String(finishCall?.body.input.history))).toEqual([
|
|
309
|
-
{ type: "message", role: "user", content: [{ type: "input_text", text: "first message" }] },
|
|
310
|
-
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "response" }] },
|
|
311
|
-
]);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("filters tool calls and tool results from history (user/assistant only)", async () => {
|
|
315
|
-
const calls = mockFetch();
|
|
316
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
317
|
-
const messages = [
|
|
318
|
-
{ role: "system", content: "you are helpful" },
|
|
319
|
-
{ role: "user", content: [{ type: "text", text: "hi" }] },
|
|
320
|
-
{
|
|
321
|
-
role: "assistant",
|
|
322
|
-
content: [
|
|
323
|
-
{ type: "text", text: "answer" },
|
|
324
|
-
{ type: "tool-call", toolCallId: "call_1", toolName: "getX", input: { id: 1 } },
|
|
325
|
-
],
|
|
326
|
-
},
|
|
327
|
-
{ role: "tool", content: [{ type: "tool-result", toolCallId: "call_1", output: "x" }] },
|
|
328
|
-
{ role: "user", content: [{ type: "text", text: "follow-up" }] },
|
|
329
|
-
];
|
|
330
|
-
|
|
331
|
-
await logger.onStart!({
|
|
332
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
333
|
-
messages,
|
|
334
|
-
} as never);
|
|
335
|
-
|
|
336
|
-
await logger.onFinish!({
|
|
337
|
-
finishReason: "stop",
|
|
338
|
-
text: "done",
|
|
339
|
-
totalUsage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: {} },
|
|
340
|
-
} as never);
|
|
341
|
-
|
|
342
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
343
|
-
// 마지막 user 메시지(follow-up)는 현재 turn이라 제외. system/tool/function_call 도 제외.
|
|
344
|
-
expect(JSON.parse(String(finishCall?.body.input.history))).toEqual([
|
|
345
|
-
{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] },
|
|
346
|
-
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "answer" }] },
|
|
347
|
-
]);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("keeps unmatched pending tool calls until a later step or finish", async () => {
|
|
351
|
-
const calls = mockFetch();
|
|
352
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
353
|
-
|
|
354
|
-
await logger.onStart!({
|
|
355
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
356
|
-
prompt: "Weather in Seoul",
|
|
357
|
-
} as never);
|
|
358
|
-
|
|
359
|
-
await logger.onStepFinish!({
|
|
360
|
-
stepNumber: 0,
|
|
361
|
-
finishReason: "tool-calls",
|
|
362
|
-
usage: { inputTokens: 200, outputTokens: 30, inputTokenDetails: {} },
|
|
363
|
-
content: [
|
|
364
|
-
{
|
|
365
|
-
type: "tool-call",
|
|
366
|
-
toolCallId: "call_1",
|
|
367
|
-
toolName: "getWeather",
|
|
368
|
-
input: { city: "Seoul" },
|
|
369
|
-
},
|
|
370
|
-
],
|
|
371
|
-
} as never);
|
|
372
|
-
|
|
373
|
-
await logger.onStepFinish!({
|
|
374
|
-
stepNumber: 1,
|
|
375
|
-
finishReason: "tool-calls",
|
|
376
|
-
usage: { inputTokens: 250, outputTokens: 20, inputTokenDetails: {} },
|
|
377
|
-
content: [],
|
|
378
|
-
} as never);
|
|
379
|
-
|
|
380
|
-
await logger.onFinish!({
|
|
381
|
-
finishReason: "stop",
|
|
382
|
-
text: "done",
|
|
383
|
-
totalUsage: { inputTokens: 450, outputTokens: 50, inputTokenDetails: {} },
|
|
384
|
-
} as never);
|
|
385
|
-
|
|
386
|
-
const toolCallStep = calls.find(
|
|
387
|
-
(c) => c.url.includes("/appendStep") && c.body.input.type === "tool_call",
|
|
388
|
-
);
|
|
389
|
-
expect(toolCallStep?.body.input).toMatchObject({
|
|
390
|
-
stepIndex: 0,
|
|
391
|
-
toolName: "getWeather",
|
|
392
|
-
toolArgs: '{"city":"Seoul"}',
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("separates overlapping runs by metadata qgridRunId", async () => {
|
|
397
|
-
const calls = mockFetch();
|
|
398
|
-
const logger = getIntegration({ serverUrl: SERVER });
|
|
399
|
-
|
|
400
|
-
await logger.onStart!({
|
|
401
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
402
|
-
prompt: "first",
|
|
403
|
-
metadata: { qgridRunId: "run-1" },
|
|
404
|
-
} as never);
|
|
405
|
-
await logger.onStart!({
|
|
406
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
407
|
-
prompt: "second",
|
|
408
|
-
metadata: { qgridRunId: "run-2" },
|
|
409
|
-
} as never);
|
|
410
|
-
|
|
411
|
-
await logger.onFinish!({
|
|
412
|
-
finishReason: "stop",
|
|
413
|
-
text: "first response",
|
|
414
|
-
totalUsage: { inputTokens: 1, outputTokens: 1, inputTokenDetails: {} },
|
|
415
|
-
metadata: { qgridRunId: "run-1" },
|
|
416
|
-
} as never);
|
|
417
|
-
await logger.onFinish!({
|
|
418
|
-
finishReason: "stop",
|
|
419
|
-
text: "second response",
|
|
420
|
-
totalUsage: { inputTokens: 2, outputTokens: 2, inputTokenDetails: {} },
|
|
421
|
-
metadata: { qgridRunId: "run-2" },
|
|
422
|
-
} as never);
|
|
423
|
-
|
|
424
|
-
const finishCalls = calls.filter((c) => c.url.includes("/finishRun"));
|
|
425
|
-
expect(finishCalls.map((c) => c.body.input.response)).toEqual([
|
|
426
|
-
"first response",
|
|
427
|
-
"second response",
|
|
428
|
-
]);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("closes the active run and quarantines when telemetry keys overlap", async () => {
|
|
432
|
-
const calls = mockFetch();
|
|
433
|
-
const errors: Error[] = [];
|
|
434
|
-
const logger = getIntegration({
|
|
435
|
-
serverUrl: SERVER,
|
|
436
|
-
onLogError: (err) => errors.push(err),
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
await logger.onStart!({
|
|
440
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
441
|
-
prompt: "first",
|
|
442
|
-
} as never);
|
|
443
|
-
await logger.onStart!({
|
|
444
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
445
|
-
prompt: "second",
|
|
446
|
-
} as never);
|
|
447
|
-
|
|
448
|
-
// 늦게 도착하는 이전 onFinish들 — quarantine 중이므로 무시됨
|
|
449
|
-
await logger.onFinish!({
|
|
450
|
-
finishReason: "stop",
|
|
451
|
-
text: "first response",
|
|
452
|
-
totalUsage: { inputTokens: 1, outputTokens: 1, inputTokenDetails: {} },
|
|
453
|
-
} as never);
|
|
454
|
-
await logger.onFinish!({
|
|
455
|
-
finishReason: "stop",
|
|
456
|
-
text: "second response",
|
|
457
|
-
totalUsage: { inputTokens: 2, outputTokens: 2, inputTokenDetails: {} },
|
|
458
|
-
} as never);
|
|
459
|
-
|
|
460
|
-
// overlap된 첫 run만 error finalize됨. 새 run은 생성 안 됨.
|
|
461
|
-
const finishCalls = calls.filter((c) => c.url.includes("/finishRun"));
|
|
462
|
-
expect(finishCalls).toHaveLength(1);
|
|
463
|
-
expect(finishCalls[0].body.input).toMatchObject({
|
|
464
|
-
status: "error",
|
|
465
|
-
errorMessage: expect.stringContaining("overlapping runs"),
|
|
466
|
-
});
|
|
467
|
-
expect(errors[0].message).toContain("overlapping runs");
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it("marks runs as error when onFinish is never emitted", async () => {
|
|
471
|
-
vi.useFakeTimers();
|
|
472
|
-
const calls = mockFetch();
|
|
473
|
-
const logger = getIntegration({ serverUrl: SERVER, staleRunTimeoutMs: 100 });
|
|
474
|
-
|
|
475
|
-
await logger.onStart!({
|
|
476
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
477
|
-
prompt: "test",
|
|
478
|
-
} as never);
|
|
479
|
-
|
|
480
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
481
|
-
|
|
482
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
483
|
-
expect(finishCall?.body.input).toMatchObject({
|
|
484
|
-
status: "error",
|
|
485
|
-
errorMessage: "AI SDK generation ended before onFinish was emitted",
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it("marks runs as aborted when the AI SDK abort signal fires", async () => {
|
|
490
|
-
const calls = mockFetch();
|
|
491
|
-
const abortController = new AbortController();
|
|
492
|
-
const logger = getIntegration({ serverUrl: SERVER, staleRunTimeoutMs: 0 });
|
|
493
|
-
|
|
494
|
-
await logger.onStart!({
|
|
495
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
496
|
-
prompt: "test",
|
|
497
|
-
abortSignal: abortController.signal,
|
|
498
|
-
} as never);
|
|
499
|
-
|
|
500
|
-
abortController.abort("user stopped");
|
|
501
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
502
|
-
|
|
503
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
504
|
-
expect(finishCall?.body.input).toMatchObject({
|
|
505
|
-
status: "aborted",
|
|
506
|
-
errorMessage: "user stopped",
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
it("expires suppressed qgrid runs when their finish event never arrives", async () => {
|
|
511
|
-
vi.useFakeTimers();
|
|
512
|
-
const calls = mockFetch();
|
|
513
|
-
const logger = getIntegration({ serverUrl: SERVER, staleRunTimeoutMs: 100 });
|
|
514
|
-
|
|
515
|
-
await logger.onStart!({
|
|
516
|
-
model: { provider: "qgrid", modelId: "openai/gpt-5.4" },
|
|
517
|
-
prompt: "handled by qgrid wrapper",
|
|
518
|
-
} as never);
|
|
519
|
-
|
|
520
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
521
|
-
|
|
522
|
-
await logger.onStart!({
|
|
523
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
524
|
-
prompt: "next normal call",
|
|
525
|
-
} as never);
|
|
526
|
-
await logger.onFinish!({
|
|
527
|
-
finishReason: "stop",
|
|
528
|
-
text: "ok",
|
|
529
|
-
totalUsage: { inputTokens: 1, outputTokens: 1, inputTokenDetails: {} },
|
|
530
|
-
} as never);
|
|
531
|
-
|
|
532
|
-
const createRunCall = calls.find((c) => c.url.includes("/createRun"));
|
|
533
|
-
expect(createRunCall?.body.input).toMatchObject({
|
|
534
|
-
userPrompt: "next normal call",
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("uses custom tokenName from config", async () => {
|
|
539
|
-
const calls = mockFetch();
|
|
540
|
-
const logger = getIntegration({ serverUrl: SERVER, tokenName: "...abc1" });
|
|
541
|
-
|
|
542
|
-
await logger.onStart!({
|
|
543
|
-
model: { provider: "google", modelId: "gemini-3-flash" },
|
|
544
|
-
prompt: "test",
|
|
545
|
-
} as never);
|
|
546
|
-
|
|
547
|
-
await logger.onStepFinish!({
|
|
548
|
-
stepNumber: 0,
|
|
549
|
-
finishReason: "stop",
|
|
550
|
-
usage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: {} },
|
|
551
|
-
content: [],
|
|
552
|
-
} as never);
|
|
553
|
-
|
|
554
|
-
await logger.onFinish!({
|
|
555
|
-
finishReason: "stop",
|
|
556
|
-
text: "ok",
|
|
557
|
-
totalUsage: { inputTokens: 10, outputTokens: 5, inputTokenDetails: {} },
|
|
558
|
-
} as never);
|
|
559
|
-
|
|
560
|
-
const finishCall = calls.find((c) => c.url.includes("/finishRun"));
|
|
561
|
-
expect(finishCall?.body.input.tokenName).toBe("...abc1");
|
|
562
|
-
});
|
|
563
|
-
});
|