@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 CHANGED
@@ -30,7 +30,8 @@ type QgridProviderOptions = {
30
30
  };
31
31
  type QgridSupportedModel = "openai/gpt-5.5" | "openai/gpt-5.4" | "openai/gpt-5.2" | "openai/gpt-5.4-mini" | "openai/gpt-5.3-codex" | "openai/gpt-5.3-codex-spark" | "anthropic/claude-haiku-4-5" | "anthropic/claude-sonnet-4" | "anthropic/claude-sonnet-4-5" | "anthropic/claude-sonnet-4-6" | "anthropic/claude-sonnet-4-7" | "anthropic/claude-opus-4" | "anthropic/claude-opus-4-1" | "anthropic/claude-opus-4-5" | "anthropic/claude-opus-4-6" | "anthropic/claude-opus-4-7";
32
32
  type QgridLoggerConfig = {
33
- serverUrl: string;
33
+ /** qgrid 서버 주소. 기본값: QGRID_URL 환경변수 또는 http://localhost:44900 */
34
+ serverUrl?: string;
34
35
  projectName?: string;
35
36
  tokenName?: string;
36
37
  /**
@@ -51,7 +52,7 @@ type QgridLoggerConfig = {
51
52
  };
52
53
  //#endregion
53
54
  //#region src/logger.d.ts
54
- declare function createQgridLogger(config: QgridLoggerConfig): TelemetrySettings;
55
+ declare function createQgridLogger(config?: QgridLoggerConfig): TelemetrySettings;
55
56
  //#endregion
56
57
  //#region src/index.d.ts
57
58
  declare function qgrid(modelId: QgridSupportedModel, config?: QgridProviderConfig): LanguageModelV3;
package/dist/index.js CHANGED
@@ -237,7 +237,9 @@ function timedKeySet() {
237
237
  }
238
238
  };
239
239
  }
240
- function createQgridLogger(config) {
240
+ function createQgridLogger(config = {}) {
241
+ const serverUrl = config.serverUrl ?? process.env.QGRID_URL ?? "http://localhost:44900";
242
+ const onLogError = config.onLogError ?? ((e) => console.warn(`[qgrid-logger] ${e.message}`));
241
243
  const runs = /* @__PURE__ */ new Map();
242
244
  const keyTtl = typeof config.staleRunTimeoutMs === "number" && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
243
245
  const suppressedQgrid = timedKeySet();
@@ -249,7 +251,7 @@ function createQgridLogger(config) {
249
251
  runs.delete(runKey);
250
252
  if (run.watchdog) clearTimeout(run.watchdog);
251
253
  run.cleanupAbortListener?.();
252
- for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(config.serverUrl, {
254
+ for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(serverUrl, {
253
255
  requestLogId: run.requestLogId,
254
256
  stepIndex: pending.stepIndex,
255
257
  type: "tool_call",
@@ -258,10 +260,10 @@ function createQgridLogger(config) {
258
260
  toolName: pending.toolName,
259
261
  toolArgs: pending.toolArgs,
260
262
  toolDurationMs: run.toolDurations.get(pending.toolCallId)
261
- }).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
263
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
262
264
  run.pendingToolCalls = [];
263
265
  await Promise.allSettled(run.pendingSteps);
264
- await finishRun(config.serverUrl, {
266
+ await finishRun(serverUrl, {
265
267
  requestLogId: run.requestLogId,
266
268
  status: result.status,
267
269
  response: result.response,
@@ -273,7 +275,7 @@ function createQgridLogger(config) {
273
275
  totalDurationMs: Date.now() - run.startTime,
274
276
  history: run.history,
275
277
  ...result.errorMessage ? { errorMessage: result.errorMessage } : {}
276
- }).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e))));
278
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e))));
277
279
  };
278
280
  let autoRunIdCounter = 0;
279
281
  const resolveRunKey = (event) => {
@@ -289,7 +291,7 @@ function createQgridLogger(config) {
289
291
  if (!event.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
290
292
  const runKey = resolveRunKey(event);
291
293
  if (quarantined.has(runKey)) {
292
- config.onLogError?.(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
294
+ onLogError(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
293
295
  return;
294
296
  }
295
297
  if (event.model.provider === "qgrid") {
@@ -303,12 +305,12 @@ function createQgridLogger(config) {
303
305
  errorMessage: msg
304
306
  });
305
307
  quarantined.add(runKey, keyTtl);
306
- config.onLogError?.(new Error(msg));
308
+ onLogError(new Error(msg));
307
309
  return;
308
310
  }
309
311
  try {
310
312
  const messages = event.messages ?? (Array.isArray(event.prompt) ? event.prompt : void 0);
311
- const result = await createRun(config.serverUrl, {
313
+ const result = await createRun(serverUrl, {
312
314
  userPrompt: extractUserPrompt(event.prompt, messages),
313
315
  systemPrompt: extractSystemPrompt(event.system),
314
316
  modelName: event.model.modelId,
@@ -358,7 +360,7 @@ function createQgridLogger(config) {
358
360
  finishing: false
359
361
  });
360
362
  } catch (e) {
361
- config.onLogError?.(e instanceof Error ? e : new Error(String(e)));
363
+ onLogError(e instanceof Error ? e : new Error(String(e)));
362
364
  }
363
365
  },
364
366
  onToolCallFinish(event) {
@@ -379,7 +381,7 @@ function createQgridLogger(config) {
379
381
  const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === pending.toolCallId);
380
382
  const te = content.find((p) => p.type === "tool-error" && p.toolCallId === pending.toolCallId);
381
383
  if (tr || te) {
382
- run.pendingSteps.push(appendStep(config.serverUrl, {
384
+ run.pendingSteps.push(appendStep(serverUrl, {
383
385
  requestLogId: run.requestLogId,
384
386
  stepIndex: pending.stepIndex,
385
387
  type: "tool_call",
@@ -390,12 +392,12 @@ function createQgridLogger(config) {
390
392
  toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
391
393
  toolDurationMs: run.toolDurations.get(pending.toolCallId),
392
394
  error: te && "error" in te ? safeStringify(te.error) : void 0
393
- }).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
395
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
394
396
  run.toolDurations.delete(pending.toolCallId);
395
397
  } else remainingPending.push(pending);
396
398
  }
397
399
  run.pendingToolCalls = remainingPending;
398
- run.pendingSteps.push(appendStep(config.serverUrl, {
400
+ run.pendingSteps.push(appendStep(serverUrl, {
399
401
  requestLogId: run.requestLogId,
400
402
  stepIndex: stepNumber,
401
403
  type: "generate",
@@ -406,13 +408,13 @@ function createQgridLogger(config) {
406
408
  finishReason,
407
409
  reasoningText: typeof reasoningText === "string" && reasoningText.length > 0 ? reasoningText : void 0,
408
410
  reasoningTokens: usage.outputTokenDetails?.reasoningTokens
409
- }).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
411
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
410
412
  const toolCalls = content.filter((p) => p.type === "tool-call");
411
413
  for (const [i, tc] of toolCalls.entries()) {
412
414
  const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === tc.toolCallId);
413
415
  const te = content.find((p) => p.type === "tool-error" && p.toolCallId === tc.toolCallId);
414
416
  if (tr || te) {
415
- run.pendingSteps.push(appendStep(config.serverUrl, {
417
+ run.pendingSteps.push(appendStep(serverUrl, {
416
418
  requestLogId: run.requestLogId,
417
419
  stepIndex: stepNumber,
418
420
  type: "tool_call",
@@ -423,7 +425,7 @@ function createQgridLogger(config) {
423
425
  toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
424
426
  toolDurationMs: run.toolDurations.get(tc.toolCallId),
425
427
  error: te && "error" in te ? safeStringify(te.error) : void 0
426
- }).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
428
+ }).catch((e) => onLogError(e instanceof Error ? e : new Error(String(e)))));
427
429
  run.toolDurations.delete(tc.toolCallId);
428
430
  } else run.pendingToolCalls.push({
429
431
  stepIndex: stepNumber,
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-ai-sdk",
3
- "version": "0.1.0",
3
+ "version": "2.0.2",
4
4
  "description": "AI SDK LanguageModelV3 provider for qgrid",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/cartanova-ai/qgrid"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
5
12
  "type": "module",
6
13
  "main": "dist/index.js",
7
14
  "types": "dist/index.d.ts",
@@ -11,21 +18,21 @@
11
18
  "types": "./dist/index.d.ts"
12
19
  }
13
20
  },
14
- "scripts": {
15
- "build": "tsdown && mv dist/index-*.d.ts dist/index.d.ts",
16
- "test": "vitest run",
17
- "e2e": "tsx e2e/e2e.ts",
18
- "e2e:logger": "tsx e2e/e2e-logger.ts"
19
- },
20
21
  "devDependencies": {
21
- "@ai-sdk/provider": "catalog:",
22
- "ai": "catalog:",
23
- "tsdown": "catalog:",
24
- "typescript": "catalog:",
25
- "vitest": "catalog:"
22
+ "@ai-sdk/provider": "^3.0.0",
23
+ "ai": "^6.0.190",
24
+ "tsdown": "^0.12.9",
25
+ "typescript": "^5.9.3",
26
+ "vitest": "^4.0.10"
26
27
  },
27
28
  "peerDependencies": {
28
29
  "@ai-sdk/provider": "^3.0.0",
29
30
  "ai": "^6.0.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown && mv dist/index-*.d.ts dist/index.d.ts",
34
+ "test": "vitest run",
35
+ "e2e": "tsx e2e/e2e.ts",
36
+ "e2e:logger": "tsx e2e/e2e-logger.ts"
30
37
  }
31
- }
38
+ }
package/e2e/e2e-logger.ts DELETED
@@ -1,112 +0,0 @@
1
- /**
2
- * @cartanova/qgrid-ai-sdk Logger E2E Test
3
- *
4
- * TelemetryIntegration 기반 logger가 실서버에 run lifecycle을 기록하는지 검증.
5
- * qgrid 서버(localhost:44900)가 떠 있어야 합니다.
6
- *
7
- * 사용법: pnpm --filter @cartanova/qgrid-ai-sdk e2e:logger
8
- */
9
- import { generateText, stepCountIs, tool } from "ai";
10
- import { z } from "zod";
11
-
12
- import { createQgridLogger, qgrid } from "../src/index";
13
-
14
- const SERVER = process.env.QGRID_URL ?? "http://localhost:44900";
15
- const MODEL = (process.env.QGRID_MODEL ?? "openai/gpt-5.5") as Parameters<typeof qgrid>[0];
16
-
17
- let passed = 0;
18
- let failed = 0;
19
-
20
- async function test(name: string, fn: () => Promise<void>) {
21
- try {
22
- await fn();
23
- passed++;
24
- console.log(` ✓ ${name}`);
25
- } catch (e) {
26
- failed++;
27
- console.error(` ✗ ${name}`);
28
- console.error(` ${(e as Error).message}`);
29
- }
30
- }
31
-
32
- function assert(condition: unknown, message: string): asserts condition {
33
- if (!condition) throw new Error(message);
34
- }
35
-
36
- const WEATHER_DB: Record<string, { temperature: number; condition: string }> = {
37
- Seoul: { temperature: 22, condition: "sunny" },
38
- Busan: { temperature: 26, condition: "partly cloudy" },
39
- };
40
-
41
- async function main() {
42
- console.log(`\n@cartanova/qgrid-ai-sdk Logger E2E (server: ${SERVER}, model: ${MODEL})\n`);
43
-
44
- // 1. 단순 텍스트 + logger
45
- await test("logger: simple text generation", async () => {
46
- const logger = createQgridLogger({
47
- serverUrl: SERVER,
48
- projectName: "e2e-logger",
49
- tokenName: "e2e-test",
50
- });
51
-
52
- const result = await generateText({
53
- model: qgrid(MODEL),
54
- prompt: 'Say exactly: "logger-ok"',
55
- experimental_telemetry: { integrations: [logger] },
56
- });
57
-
58
- assert(result.text.length > 0, "empty response");
59
- console.log(` text="${result.text.slice(0, 50)}"`);
60
-
61
- // 대기 후 DB 검증
62
- await new Promise((r) => setTimeout(r, 1500));
63
-
64
- const logRes = await fetch(
65
- `${SERVER}/api/requestLog/findMany?subset=A&rawParams%5Bnum%5D=1&rawParams%5Bpage%5D=1&rawParams%5BorderBy%5D=id-desc`,
66
- );
67
- const logData = (await logRes.json()) as { rows: Array<Record<string, unknown>> };
68
- const log = logData.rows[0];
69
-
70
- // qgrid provider가 이미 lifecycle을 관리하므로 logger는 skip해야 함
71
- // 하지만 단순 호출이라 qgrid provider의 runState도 생성 안 됨 (tools 없음)
72
- // 이 경우 둘 다 기록하지 않거나, qgrid provider만 기록
73
- console.log(` latest log id=${log?.id}, status=${log?.status}`);
74
- });
75
-
76
- // 2. tool calling + logger (qgrid provider 사용 — logger가 skip하는지 검증)
77
- await test("logger: skips when qgrid provider is used", async () => {
78
- const errors: Error[] = [];
79
- const logger = createQgridLogger({
80
- serverUrl: SERVER,
81
- projectName: "e2e-logger",
82
- onLogError: (err) => errors.push(err),
83
- });
84
-
85
- const result = await generateText({
86
- model: qgrid(MODEL),
87
- prompt: "What is the weather in Seoul? Use the getWeather tool.",
88
- tools: {
89
- getWeather: tool({
90
- description: "Get weather for a city",
91
- inputSchema: z.object({ city: z.string() }),
92
- execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
93
- }),
94
- },
95
- stopWhen: stepCountIs(3),
96
- experimental_telemetry: { integrations: [logger] },
97
- });
98
-
99
- assert(result.text.length > 0, "empty response");
100
- assert(errors.length === 0, `logger errors: ${errors.map((e) => e.message).join(", ")}`);
101
- console.log(` text="${result.text.slice(0, 80)}"`);
102
- console.log(` logger skipped (qgrid provider detected)`);
103
- });
104
-
105
- console.log(`\n ${passed} passed, ${failed} failed\n`);
106
- if (failed > 0) process.exit(1);
107
- }
108
-
109
- main().catch((e) => {
110
- console.error("E2E failed:", e);
111
- process.exit(1);
112
- });
package/e2e/e2e.ts DELETED
@@ -1,217 +0,0 @@
1
- /**
2
- * @cartanova/qgrid-ai-sdk E2E Test Suite
3
- *
4
- * 실제 qgrid 서버(localhost:44900)에 요청을 보내는 통합 테스트.
5
- * 서버가 떠 있어야 합니다.
6
- *
7
- * 사용법: pnpm --filter @cartanova/qgrid-ai-sdk e2e
8
- */
9
- import { generateText, stepCountIs, tool } from "ai";
10
- import { z } from "zod";
11
-
12
- import { qgrid } from "../src/index";
13
-
14
- const SERVER = process.env.QGRID_URL ?? "http://localhost:44900";
15
- const MODEL = (process.env.QGRID_MODEL ?? "openai/gpt-5.5") as Parameters<typeof qgrid>[0];
16
-
17
- // ── Test runner ────────────────────────────────────────────────────
18
-
19
- let passed = 0;
20
- let failed = 0;
21
-
22
- async function test(name: string, fn: () => Promise<void>) {
23
- try {
24
- await fn();
25
- passed++;
26
- console.log(` ✓ ${name}`);
27
- } catch (e) {
28
- failed++;
29
- console.error(` ✗ ${name}`);
30
- console.error(` ${(e as Error).message}`);
31
- }
32
- }
33
-
34
- function assert(condition: unknown, message: string): asserts condition {
35
- if (!condition) throw new Error(message);
36
- }
37
-
38
- // ── Mock data ──────────────────────────────────────────────────────
39
-
40
- const WEATHER_DB: Record<string, { temperature: number; condition: string }> = {
41
- Seoul: { temperature: 22, condition: "sunny" },
42
- Busan: { temperature: 26, condition: "partly cloudy" },
43
- Daegu: { temperature: 28, condition: "hot" },
44
- Jeju: { temperature: 24, condition: "rainy" },
45
- };
46
-
47
- const RESTAURANT_DB: Record<
48
- string,
49
- Array<{ id: string; name: string; cuisine: string; rating: number }>
50
- > = {
51
- Daegu: [
52
- { id: "r6", name: "대구 막창", cuisine: "BBQ", rating: 4.4 },
53
- { id: "r7", name: "안지랑 곱창", cuisine: "Korean", rating: 4.7 },
54
- ],
55
- Busan: [
56
- { id: "r3", name: "해운대 회센터", cuisine: "Seafood", rating: 4.7 },
57
- { id: "r4", name: "서면 돼지국밥", cuisine: "Korean", rating: 4.9 },
58
- ],
59
- };
60
-
61
- // ── Tests ──────────────────────────────────────────────────────────
62
-
63
- async function main() {
64
- console.log(`\n@cartanova/qgrid-ai-sdk E2E (server: ${SERVER}, model: ${MODEL})\n`);
65
-
66
- // 1. 단순 텍스트
67
- await test("simple text generation", async () => {
68
- const result = await generateText({
69
- model: qgrid(MODEL),
70
- prompt: 'Respond with exactly: "hello-e2e"',
71
- });
72
- assert(result.text.length > 0, "empty response");
73
- assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
74
- assert(result.usage?.inputTokens, "missing input token usage");
75
- console.log(` text="${result.text.slice(0, 80)}"`);
76
- });
77
-
78
- // 2. system prompt
79
- await test("system prompt", async () => {
80
- const result = await generateText({
81
- model: qgrid(MODEL),
82
- system: "You must reply in exactly one word.",
83
- prompt: "What color is the sky?",
84
- });
85
- assert(result.text.length > 0, "empty response");
86
- console.log(` text="${result.text.slice(0, 80)}"`);
87
- });
88
-
89
- // 3. single tool call
90
- await test("single tool call", async () => {
91
- const result = await generateText({
92
- model: qgrid(MODEL),
93
- prompt: "What is the weather in Seoul? Use the getWeather tool.",
94
- tools: {
95
- getWeather: tool({
96
- description: "Get current weather for a city",
97
- inputSchema: z.object({ city: z.string() }),
98
- execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
99
- }),
100
- },
101
- stopWhen: stepCountIs(3),
102
- });
103
- assert(result.steps.length >= 2, `expected at least 2 steps, got ${result.steps.length}`);
104
- assert(result.steps[0].toolCalls.length > 0, "step 0 should have tool calls");
105
- assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
106
- assert(result.text.length > 0, "empty final text");
107
- console.log(` steps=${result.steps.length}, text="${result.text.slice(0, 80)}"`);
108
- });
109
-
110
- // 4. multi-tool, multi-step
111
- await test("multi-tool multi-step", async () => {
112
- const result = await generateText({
113
- model: qgrid(MODEL),
114
- prompt:
115
- "Step 1: Use the getWeather tool for Seoul, Busan, and Daegu. Step 2: Identify the warmest city. Step 3: Use the searchRestaurants tool for that warmest city. Then give a final summary.",
116
- tools: {
117
- getWeather: tool({
118
- description: "Get weather for a city (English name: Seoul, Busan, Daegu, Jeju)",
119
- inputSchema: z.object({ city: z.string() }),
120
- execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
121
- }),
122
- searchRestaurants: tool({
123
- description: "Search restaurants in a city",
124
- inputSchema: z.object({ city: z.string() }),
125
- execute: async ({ city }) => RESTAURANT_DB[city] ?? [],
126
- }),
127
- },
128
- stopWhen: stepCountIs(5),
129
- });
130
-
131
- const allToolCalls = result.steps.flatMap((s) => s.toolCalls);
132
- const weatherCalls = allToolCalls.filter((tc) => tc.toolName === "getWeather");
133
- const restaurantCalls = allToolCalls.filter((tc) => tc.toolName === "searchRestaurants");
134
- const toolNames = [...new Set(allToolCalls.map((tc) => tc.toolName))];
135
-
136
- assert(
137
- allToolCalls.length >= 2,
138
- `expected at least 2 total tool calls, got ${allToolCalls.length}`,
139
- );
140
- assert(
141
- weatherCalls.length >= 2,
142
- `expected at least 2 getWeather calls, got ${weatherCalls.length}`,
143
- );
144
- assert(result.steps.length >= 2, `expected at least 2 steps, got ${result.steps.length}`);
145
- assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
146
- console.log(
147
- ` steps=${result.steps.length}, tools=${toolNames.join(",")}, weatherCalls=${weatherCalls.length}, restaurantCalls=${restaurantCalls.length}`,
148
- );
149
- console.log(` text="${result.text.slice(0, 100)}"`);
150
- });
151
-
152
- // 5. auto lifecycle — run이 DB에 기록되었는지 검증
153
- await test("auto lifecycle (createRun → appendStep → finishRun)", async () => {
154
- const result = await generateText({
155
- model: qgrid(MODEL),
156
- prompt: "What is the weather in Busan? Use the getWeather tool.",
157
- tools: {
158
- getWeather: tool({
159
- description: "Get weather for a city",
160
- inputSchema: z.object({ city: z.string() }),
161
- execute: async ({ city }) => WEATHER_DB[city] ?? { temperature: 0, condition: "unknown" },
162
- }),
163
- },
164
- stopWhen: stepCountIs(3),
165
- });
166
- assert(result.finishReason === "stop", `unexpected finishReason: ${result.finishReason}`);
167
-
168
- // fire-and-forget appendStep 완료 대기
169
- await new Promise((r) => setTimeout(r, 1500));
170
-
171
- // 가장 최근 request_log 조회
172
- const logRes = await fetch(
173
- `${SERVER}/api/requestLog/findMany?subset=A&rawParams%5Bnum%5D=1&rawParams%5Bpage%5D=1&rawParams%5BorderBy%5D=id-desc`,
174
- );
175
- const logData = (await logRes.json()) as { rows: Array<Record<string, unknown>> };
176
- const log = logData.rows[0];
177
- assert(log, "no request_log found");
178
- assert(log.status === "succeeded", `expected status=succeeded, got ${log.status}`);
179
- assert((log.input_tokens as number) > 0, "input_tokens should be > 0");
180
- assert(
181
- (log.tool_call_count as number) >= 1,
182
- `expected tool_call_count >= 1, got ${log.tool_call_count}`,
183
- );
184
- assert((log.response as string)?.length > 0, "response should not be empty");
185
-
186
- // steps 검증
187
- const stepsRes = await fetch(
188
- `${SERVER}/api/requestLogStep/findMany?subset=A&rawParams%5Bnum%5D=50&rawParams%5Bpage%5D=1&rawParams%5Brequest_log_id%5D=${log.id}&rawParams%5BorderBy%5D=id-asc`,
189
- );
190
- const stepsData = (await stepsRes.json()) as {
191
- rows: Array<Record<string, unknown>>;
192
- total: number;
193
- };
194
- const generateSteps = stepsData.rows.filter((s) => s.type === "generate");
195
- const toolSteps = stepsData.rows.filter((s) => s.type === "tool_call");
196
-
197
- assert(
198
- generateSteps.length >= 2,
199
- `expected at least 2 generate steps, got ${generateSteps.length}`,
200
- );
201
- assert(toolSteps.length >= 1, `expected at least 1 tool_call step, got ${toolSteps.length}`);
202
-
203
- console.log(
204
- ` requestLogId=${log.id}, status=${log.status}, tool_call_count=${log.tool_call_count}, steps=${stepsData.total}`,
205
- );
206
- });
207
-
208
- // ── Summary ──────────────────────────────────────────────────────
209
-
210
- console.log(`\n ${passed} passed, ${failed} failed\n`);
211
- if (failed > 0) process.exit(1);
212
- }
213
-
214
- main().catch((e) => {
215
- console.error("E2E failed:", e);
216
- process.exit(1);
217
- });