@cartanova/qgrid-ai-sdk 0.1.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/README.md +250 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +750 -0
- package/e2e/e2e-logger.ts +112 -0
- package/e2e/e2e.ts +217 -0
- package/package.json +31 -0
- package/src/index.test.ts +338 -0
- package/src/index.ts +396 -0
- package/src/index.types.ts +131 -0
- package/src/logger.test.ts +563 -0
- package/src/logger.ts +364 -0
- package/src/utils.ts +305 -0
- package/tsconfig.json +15 -0
- package/tsdown.config.ts +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
//#region src/utils.ts
|
|
2
|
+
async function createRun(serverUrl, body) {
|
|
3
|
+
return (await fetch(`${serverUrl}/api/qgrid/createRun`, {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: { "Content-Type": "application/json" },
|
|
6
|
+
body: JSON.stringify({ input: body })
|
|
7
|
+
})).json();
|
|
8
|
+
}
|
|
9
|
+
async function appendStep(serverUrl, body) {
|
|
10
|
+
return (await fetch(`${serverUrl}/api/qgrid/appendStep`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: { "Content-Type": "application/json" },
|
|
13
|
+
body: JSON.stringify({ input: body })
|
|
14
|
+
})).json();
|
|
15
|
+
}
|
|
16
|
+
async function finishRun(serverUrl, body) {
|
|
17
|
+
return (await fetch(`${serverUrl}/api/qgrid/finishRun`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ input: body })
|
|
21
|
+
})).json();
|
|
22
|
+
}
|
|
23
|
+
function toQgridTool(tool) {
|
|
24
|
+
const source = tool;
|
|
25
|
+
return {
|
|
26
|
+
name: tool.name,
|
|
27
|
+
description: tool.description,
|
|
28
|
+
inputSchema: source.inputSchema ?? source.parameters ?? {}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function extractToolResultsFromHistory(messages) {
|
|
32
|
+
const calls = /* @__PURE__ */ new Map();
|
|
33
|
+
const results = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const msg of messages) if (msg.role === "assistant") {
|
|
35
|
+
for (const part of msg.content) if ("type" in part && part.type === "tool-call") calls.set(part.toolCallId, {
|
|
36
|
+
toolName: part.toolName,
|
|
37
|
+
args: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
|
|
38
|
+
});
|
|
39
|
+
} else if (msg.role === "tool") {
|
|
40
|
+
for (const part of msg.content) if ("type" in part && part.type === "tool-result") {
|
|
41
|
+
const id = "toolCallId" in part ? part.toolCallId : "";
|
|
42
|
+
const output = part.output;
|
|
43
|
+
const text = "value" in output ? typeof output.value === "string" ? output.value : JSON.stringify(output.value) : JSON.stringify(output);
|
|
44
|
+
results.set(id, text);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const [callId, call] of calls) if (results.has(callId)) out.push({
|
|
49
|
+
callId,
|
|
50
|
+
toolName: call.toolName,
|
|
51
|
+
args: call.args,
|
|
52
|
+
result: results.get(callId)
|
|
53
|
+
});
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
async function* parseSSE(body) {
|
|
57
|
+
const reader = body.getReader();
|
|
58
|
+
const decoder = new TextDecoder();
|
|
59
|
+
let buffer = "";
|
|
60
|
+
let eventType = "";
|
|
61
|
+
for (;;) {
|
|
62
|
+
const { done, value } = await reader.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
buffer += decoder.decode(value, { stream: true });
|
|
65
|
+
const lines = buffer.split("\n");
|
|
66
|
+
buffer = lines.pop() ?? "";
|
|
67
|
+
for (const line of lines) if (line.startsWith("event: ")) eventType = line.slice(7).trim();
|
|
68
|
+
else if (line.startsWith("data: ")) {
|
|
69
|
+
const raw = line.slice(6);
|
|
70
|
+
try {
|
|
71
|
+
yield {
|
|
72
|
+
type: eventType || "message",
|
|
73
|
+
data: JSON.parse(raw)
|
|
74
|
+
};
|
|
75
|
+
} catch {}
|
|
76
|
+
eventType = "";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function extractPromptAndHistory(messages) {
|
|
81
|
+
let system;
|
|
82
|
+
const nonSystem = [];
|
|
83
|
+
for (const msg of messages) if (msg.role === "system") system = extractTextFromContent(msg.content);
|
|
84
|
+
else nonSystem.push(msg);
|
|
85
|
+
if (nonSystem.length === 0) return {
|
|
86
|
+
prompt: "",
|
|
87
|
+
system,
|
|
88
|
+
history: []
|
|
89
|
+
};
|
|
90
|
+
if (nonSystem.length === 1 && nonSystem[0].role === "user") return {
|
|
91
|
+
prompt: extractTextFromContent(nonSystem[0].content),
|
|
92
|
+
system,
|
|
93
|
+
history: []
|
|
94
|
+
};
|
|
95
|
+
const last = nonSystem[nonSystem.length - 1];
|
|
96
|
+
const prompt = last.role === "user" ? extractTextFromContent(last.content) : "";
|
|
97
|
+
const historyEnd = last.role === "user" ? nonSystem.length - 1 : nonSystem.length;
|
|
98
|
+
const history = [];
|
|
99
|
+
for (let i = 0; i < historyEnd; i++) {
|
|
100
|
+
const msg = nonSystem[i];
|
|
101
|
+
if (msg.role === "user") history.push({
|
|
102
|
+
type: "message",
|
|
103
|
+
role: "user",
|
|
104
|
+
content: [{
|
|
105
|
+
type: "input_text",
|
|
106
|
+
text: extractTextFromContent(msg.content)
|
|
107
|
+
}]
|
|
108
|
+
});
|
|
109
|
+
else if (msg.role === "assistant") {
|
|
110
|
+
for (const part of msg.content) if ("text" in part && typeof part.text === "string") history.push({
|
|
111
|
+
type: "message",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: [{
|
|
114
|
+
type: "output_text",
|
|
115
|
+
text: part.text
|
|
116
|
+
}]
|
|
117
|
+
});
|
|
118
|
+
else if ("toolName" in part && part.type === "tool-call") history.push({
|
|
119
|
+
type: "function_call",
|
|
120
|
+
name: part.toolName,
|
|
121
|
+
arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input),
|
|
122
|
+
call_id: part.toolCallId
|
|
123
|
+
});
|
|
124
|
+
} else if (msg.role === "tool") {
|
|
125
|
+
for (const part of msg.content) if ("toolName" in part && part.type === "tool-result") {
|
|
126
|
+
const output = part.output;
|
|
127
|
+
const text = "value" in output ? typeof output.value === "string" ? output.value : JSON.stringify(output.value) : JSON.stringify(output);
|
|
128
|
+
history.push({
|
|
129
|
+
type: "function_call_output",
|
|
130
|
+
call_id: ("toolCallId" in part ? part.toolCallId : "") ?? "",
|
|
131
|
+
output: text
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
prompt,
|
|
138
|
+
system,
|
|
139
|
+
history
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function extractTextFromContent(content) {
|
|
143
|
+
if (typeof content === "string") return content;
|
|
144
|
+
const parts = [];
|
|
145
|
+
for (const part of content) if ("text" in part && typeof part.text === "string") parts.push(part.text);
|
|
146
|
+
return parts.join("\n");
|
|
147
|
+
}
|
|
148
|
+
function safeStringify(value) {
|
|
149
|
+
try {
|
|
150
|
+
const json = JSON.stringify(value);
|
|
151
|
+
return json === void 0 ? String(value) : json;
|
|
152
|
+
} catch {
|
|
153
|
+
return String(value);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function getRecord(value) {
|
|
157
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
158
|
+
}
|
|
159
|
+
function extractTextContent(content) {
|
|
160
|
+
if (typeof content === "string") return content;
|
|
161
|
+
const parts = [];
|
|
162
|
+
if (Array.isArray(content)) {
|
|
163
|
+
for (const part of content) if (part && typeof part === "object" && "type" in part) {
|
|
164
|
+
if (part.type === "text" && "text" in part && typeof part.text === "string") parts.push(part.text);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return parts.join("\n");
|
|
168
|
+
}
|
|
169
|
+
function getErrorMessage(value) {
|
|
170
|
+
if (value instanceof Error) return String(value);
|
|
171
|
+
if (value === void 0 || value === null) return "unknown error";
|
|
172
|
+
return String(value);
|
|
173
|
+
}
|
|
174
|
+
function extractUserPrompt(prompt, messages) {
|
|
175
|
+
if (typeof prompt === "string") return prompt;
|
|
176
|
+
const messageList = Array.isArray(messages) ? messages : Array.isArray(prompt) ? prompt : [];
|
|
177
|
+
for (let i = messageList.length - 1; i >= 0; i--) {
|
|
178
|
+
const msg = getRecord(messageList[i]);
|
|
179
|
+
if (msg?.role === "user") return extractTextContent(msg.content);
|
|
180
|
+
}
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
function extractSystemPrompt(system) {
|
|
184
|
+
if (typeof system === "string") return system;
|
|
185
|
+
if (system && typeof system === "object" && "content" in system) {
|
|
186
|
+
const content = system.content;
|
|
187
|
+
if (typeof content === "string") return content;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function serializeHistory(messages) {
|
|
191
|
+
if (!Array.isArray(messages) || messages.length === 0) return void 0;
|
|
192
|
+
const sliced = getRecord(messages[messages.length - 1])?.role === "user" ? messages.slice(0, -1) : messages;
|
|
193
|
+
const history = [];
|
|
194
|
+
for (const msg of sliced) {
|
|
195
|
+
const record = getRecord(msg);
|
|
196
|
+
if (record?.role !== "user" && record?.role !== "assistant") continue;
|
|
197
|
+
const text = extractTextContent(record.content);
|
|
198
|
+
if (text.length === 0) continue;
|
|
199
|
+
history.push({
|
|
200
|
+
type: "message",
|
|
201
|
+
role: record.role,
|
|
202
|
+
content: [{
|
|
203
|
+
type: record.role === "user" ? "input_text" : "output_text",
|
|
204
|
+
text
|
|
205
|
+
}]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (history.length === 0) return void 0;
|
|
209
|
+
return safeStringify(history);
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/logger.ts
|
|
213
|
+
const DEFAULT_RUN_KEY = "__qgrid_default_run__";
|
|
214
|
+
const DEFAULT_STALE_RUN_TIMEOUT_MS = 1800 * 1e3;
|
|
215
|
+
const STALE_RUN_GRACE_MS = 5e3;
|
|
216
|
+
function timedKeySet() {
|
|
217
|
+
const keys = /* @__PURE__ */ new Set();
|
|
218
|
+
const timers = /* @__PURE__ */ new Map();
|
|
219
|
+
return {
|
|
220
|
+
has: (k) => keys.has(k),
|
|
221
|
+
add(k, ttlMs) {
|
|
222
|
+
keys.add(k);
|
|
223
|
+
const existing = timers.get(k);
|
|
224
|
+
if (existing) clearTimeout(existing);
|
|
225
|
+
const t = setTimeout(() => {
|
|
226
|
+
keys.delete(k);
|
|
227
|
+
timers.delete(k);
|
|
228
|
+
}, ttlMs);
|
|
229
|
+
t.unref?.();
|
|
230
|
+
timers.set(k, t);
|
|
231
|
+
},
|
|
232
|
+
remove(k) {
|
|
233
|
+
keys.delete(k);
|
|
234
|
+
const t = timers.get(k);
|
|
235
|
+
if (t) clearTimeout(t);
|
|
236
|
+
timers.delete(k);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function createQgridLogger(config) {
|
|
241
|
+
const runs = /* @__PURE__ */ new Map();
|
|
242
|
+
const keyTtl = typeof config.staleRunTimeoutMs === "number" && config.staleRunTimeoutMs > 0 ? config.staleRunTimeoutMs : DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
243
|
+
const suppressedQgrid = timedKeySet();
|
|
244
|
+
const quarantined = timedKeySet();
|
|
245
|
+
const finalizeRun = async (runKey, result) => {
|
|
246
|
+
const run = runs.get(runKey);
|
|
247
|
+
if (!run || run.finishing) return;
|
|
248
|
+
run.finishing = true;
|
|
249
|
+
runs.delete(runKey);
|
|
250
|
+
if (run.watchdog) clearTimeout(run.watchdog);
|
|
251
|
+
run.cleanupAbortListener?.();
|
|
252
|
+
for (const pending of run.pendingToolCalls) run.pendingSteps.push(appendStep(config.serverUrl, {
|
|
253
|
+
requestLogId: run.requestLogId,
|
|
254
|
+
stepIndex: pending.stepIndex,
|
|
255
|
+
type: "tool_call",
|
|
256
|
+
toolCallIndex: pending.toolCallIndex,
|
|
257
|
+
toolCallId: pending.toolCallId,
|
|
258
|
+
toolName: pending.toolName,
|
|
259
|
+
toolArgs: pending.toolArgs,
|
|
260
|
+
toolDurationMs: run.toolDurations.get(pending.toolCallId)
|
|
261
|
+
}).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
|
|
262
|
+
run.pendingToolCalls = [];
|
|
263
|
+
await Promise.allSettled(run.pendingSteps);
|
|
264
|
+
await finishRun(config.serverUrl, {
|
|
265
|
+
requestLogId: run.requestLogId,
|
|
266
|
+
status: result.status,
|
|
267
|
+
response: result.response,
|
|
268
|
+
tokenName: config.tokenName ?? "external",
|
|
269
|
+
totalInputTokens: result.totalUsage?.inputTokens ?? 0,
|
|
270
|
+
totalOutputTokens: result.totalUsage?.outputTokens ?? 0,
|
|
271
|
+
totalCacheReadTokens: result.totalUsage?.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
272
|
+
totalCacheCreationTokens: result.totalUsage?.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
273
|
+
totalDurationMs: Date.now() - run.startTime,
|
|
274
|
+
history: run.history,
|
|
275
|
+
...result.errorMessage ? { errorMessage: result.errorMessage } : {}
|
|
276
|
+
}).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e))));
|
|
277
|
+
};
|
|
278
|
+
let autoRunIdCounter = 0;
|
|
279
|
+
const resolveRunKey = (event) => {
|
|
280
|
+
const qgridRunId = event.metadata?.qgridRunId;
|
|
281
|
+
if (typeof qgridRunId === "string" && qgridRunId.length > 0) return `qgridRunId:${qgridRunId}`;
|
|
282
|
+
if (typeof event.functionId === "string" && event.functionId.length > 0) return `functionId:${event.functionId}`;
|
|
283
|
+
return DEFAULT_RUN_KEY;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
isEnabled: true,
|
|
287
|
+
integrations: [{
|
|
288
|
+
async onStart(event) {
|
|
289
|
+
if (!event.metadata?.qgridRunId && !event.functionId && event.metadata) event.metadata.qgridRunId = `auto-${++autoRunIdCounter}`;
|
|
290
|
+
const runKey = resolveRunKey(event);
|
|
291
|
+
if (quarantined.has(runKey)) {
|
|
292
|
+
config.onLogError?.(/* @__PURE__ */ new Error("createQgridLogger: telemetry key is quarantined after overlap"));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (event.model.provider === "qgrid") {
|
|
296
|
+
suppressedQgrid.add(runKey, keyTtl);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (runs.has(runKey)) {
|
|
300
|
+
const msg = "createQgridLogger received overlapping runs for the same telemetry key. Pass a unique metadata.qgridRunId per AI SDK call or create a fresh logger integration per call.";
|
|
301
|
+
await finalizeRun(runKey, {
|
|
302
|
+
status: "error",
|
|
303
|
+
errorMessage: msg
|
|
304
|
+
});
|
|
305
|
+
quarantined.add(runKey, keyTtl);
|
|
306
|
+
config.onLogError?.(new Error(msg));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const messages = event.messages ?? (Array.isArray(event.prompt) ? event.prompt : void 0);
|
|
311
|
+
const result = await createRun(config.serverUrl, {
|
|
312
|
+
userPrompt: extractUserPrompt(event.prompt, messages),
|
|
313
|
+
systemPrompt: extractSystemPrompt(event.system),
|
|
314
|
+
modelName: event.model.modelId,
|
|
315
|
+
projectName: config.projectName
|
|
316
|
+
});
|
|
317
|
+
let watchdogTimeout = DEFAULT_STALE_RUN_TIMEOUT_MS;
|
|
318
|
+
if (typeof config.staleRunTimeoutMs === "number") watchdogTimeout = config.staleRunTimeoutMs;
|
|
319
|
+
else if (typeof event.timeout === "number" && event.timeout > 0) watchdogTimeout = event.timeout + STALE_RUN_GRACE_MS;
|
|
320
|
+
else {
|
|
321
|
+
const totalMs = getRecord(event.timeout)?.totalMs;
|
|
322
|
+
if (typeof totalMs === "number" && totalMs > 0) watchdogTimeout = totalMs + STALE_RUN_GRACE_MS;
|
|
323
|
+
}
|
|
324
|
+
let watchdog;
|
|
325
|
+
if (watchdogTimeout > 0) {
|
|
326
|
+
watchdog = setTimeout(() => {
|
|
327
|
+
finalizeRun(runKey, {
|
|
328
|
+
status: "error",
|
|
329
|
+
errorMessage: "AI SDK generation ended before onFinish was emitted"
|
|
330
|
+
});
|
|
331
|
+
}, watchdogTimeout);
|
|
332
|
+
watchdog.unref?.();
|
|
333
|
+
}
|
|
334
|
+
let cleanupAbortListener;
|
|
335
|
+
const signal = event.abortSignal;
|
|
336
|
+
if (signal) {
|
|
337
|
+
const onAbort = () => {
|
|
338
|
+
finalizeRun(runKey, {
|
|
339
|
+
status: "aborted",
|
|
340
|
+
errorMessage: getErrorMessage(signal.reason)
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
if (signal.aborted) queueMicrotask(onAbort);
|
|
344
|
+
else {
|
|
345
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
346
|
+
cleanupAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
runs.set(runKey, {
|
|
350
|
+
requestLogId: result.requestLogId,
|
|
351
|
+
pendingSteps: [],
|
|
352
|
+
pendingToolCalls: [],
|
|
353
|
+
startTime: Date.now(),
|
|
354
|
+
toolDurations: /* @__PURE__ */ new Map(),
|
|
355
|
+
history: serializeHistory(messages),
|
|
356
|
+
watchdog,
|
|
357
|
+
cleanupAbortListener,
|
|
358
|
+
finishing: false
|
|
359
|
+
});
|
|
360
|
+
} catch (e) {
|
|
361
|
+
config.onLogError?.(e instanceof Error ? e : new Error(String(e)));
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
onToolCallFinish(event) {
|
|
365
|
+
const runKey = resolveRunKey(event);
|
|
366
|
+
if (suppressedQgrid.has(runKey) || quarantined.has(runKey)) return;
|
|
367
|
+
const run = runs.get(runKey);
|
|
368
|
+
if (!run) return;
|
|
369
|
+
run.toolDurations.set(event.toolCall.toolCallId, Math.round(event.durationMs));
|
|
370
|
+
},
|
|
371
|
+
onStepFinish(event) {
|
|
372
|
+
const runKey = resolveRunKey(event);
|
|
373
|
+
if (suppressedQgrid.has(runKey) || quarantined.has(runKey)) return;
|
|
374
|
+
const run = runs.get(runKey);
|
|
375
|
+
if (!run) return;
|
|
376
|
+
const { content, usage, reasoningText, finishReason, stepNumber } = event;
|
|
377
|
+
const remainingPending = [];
|
|
378
|
+
for (const pending of run.pendingToolCalls) {
|
|
379
|
+
const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === pending.toolCallId);
|
|
380
|
+
const te = content.find((p) => p.type === "tool-error" && p.toolCallId === pending.toolCallId);
|
|
381
|
+
if (tr || te) {
|
|
382
|
+
run.pendingSteps.push(appendStep(config.serverUrl, {
|
|
383
|
+
requestLogId: run.requestLogId,
|
|
384
|
+
stepIndex: pending.stepIndex,
|
|
385
|
+
type: "tool_call",
|
|
386
|
+
toolCallIndex: pending.toolCallIndex,
|
|
387
|
+
toolCallId: pending.toolCallId,
|
|
388
|
+
toolName: pending.toolName,
|
|
389
|
+
toolArgs: pending.toolArgs,
|
|
390
|
+
toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
|
|
391
|
+
toolDurationMs: run.toolDurations.get(pending.toolCallId),
|
|
392
|
+
error: te && "error" in te ? safeStringify(te.error) : void 0
|
|
393
|
+
}).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
|
|
394
|
+
run.toolDurations.delete(pending.toolCallId);
|
|
395
|
+
} else remainingPending.push(pending);
|
|
396
|
+
}
|
|
397
|
+
run.pendingToolCalls = remainingPending;
|
|
398
|
+
run.pendingSteps.push(appendStep(config.serverUrl, {
|
|
399
|
+
requestLogId: run.requestLogId,
|
|
400
|
+
stepIndex: stepNumber,
|
|
401
|
+
type: "generate",
|
|
402
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
403
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
404
|
+
cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
|
|
405
|
+
cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
|
|
406
|
+
finishReason,
|
|
407
|
+
reasoningText: typeof reasoningText === "string" && reasoningText.length > 0 ? reasoningText : void 0,
|
|
408
|
+
reasoningTokens: usage.outputTokenDetails?.reasoningTokens
|
|
409
|
+
}).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
|
|
410
|
+
const toolCalls = content.filter((p) => p.type === "tool-call");
|
|
411
|
+
for (const [i, tc] of toolCalls.entries()) {
|
|
412
|
+
const tr = content.find((p) => p.type === "tool-result" && p.toolCallId === tc.toolCallId);
|
|
413
|
+
const te = content.find((p) => p.type === "tool-error" && p.toolCallId === tc.toolCallId);
|
|
414
|
+
if (tr || te) {
|
|
415
|
+
run.pendingSteps.push(appendStep(config.serverUrl, {
|
|
416
|
+
requestLogId: run.requestLogId,
|
|
417
|
+
stepIndex: stepNumber,
|
|
418
|
+
type: "tool_call",
|
|
419
|
+
toolCallIndex: i,
|
|
420
|
+
toolCallId: tc.toolCallId,
|
|
421
|
+
toolName: tc.toolName,
|
|
422
|
+
toolArgs: safeStringify(tc.input),
|
|
423
|
+
toolResult: tr && "output" in tr ? safeStringify(tr.output) : void 0,
|
|
424
|
+
toolDurationMs: run.toolDurations.get(tc.toolCallId),
|
|
425
|
+
error: te && "error" in te ? safeStringify(te.error) : void 0
|
|
426
|
+
}).catch((e) => config.onLogError?.(e instanceof Error ? e : new Error(String(e)))));
|
|
427
|
+
run.toolDurations.delete(tc.toolCallId);
|
|
428
|
+
} else run.pendingToolCalls.push({
|
|
429
|
+
stepIndex: stepNumber,
|
|
430
|
+
toolCallIndex: i,
|
|
431
|
+
toolCallId: tc.toolCallId,
|
|
432
|
+
toolName: tc.toolName,
|
|
433
|
+
toolArgs: safeStringify(tc.input)
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
async onFinish(event) {
|
|
438
|
+
const runKey = resolveRunKey(event);
|
|
439
|
+
if (suppressedQgrid.has(runKey)) {
|
|
440
|
+
suppressedQgrid.remove(runKey);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (quarantined.has(runKey)) return;
|
|
444
|
+
if (!runs.get(runKey)) return;
|
|
445
|
+
const status = event.finishReason === "error" ? "error" : "succeeded";
|
|
446
|
+
await finalizeRun(runKey, {
|
|
447
|
+
status,
|
|
448
|
+
response: event.text,
|
|
449
|
+
totalUsage: event.totalUsage,
|
|
450
|
+
...status === "error" && "error" in event ? { errorMessage: getErrorMessage(event.error) } : {}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}]
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/index.ts
|
|
458
|
+
const DEFAULT_SERVER_URL = "http://localhost:44900";
|
|
459
|
+
const DEFAULT_EFFORT = "low";
|
|
460
|
+
function qgrid(modelId, config) {
|
|
461
|
+
const serverUrl = config?.serverUrl ?? process.env.QGRID_URL ?? DEFAULT_SERVER_URL;
|
|
462
|
+
const effort = config?.defaultEffort ?? DEFAULT_EFFORT;
|
|
463
|
+
let clientRun = null;
|
|
464
|
+
return {
|
|
465
|
+
specificationVersion: "v3",
|
|
466
|
+
provider: "qgrid",
|
|
467
|
+
modelId,
|
|
468
|
+
supportedUrls: {},
|
|
469
|
+
async doGenerate(options) {
|
|
470
|
+
const tools = options.tools?.filter((t) => t.type === "function");
|
|
471
|
+
const hasTools = tools && tools.length > 0;
|
|
472
|
+
const { prompt, system, history } = extractPromptAndHistory(options.prompt);
|
|
473
|
+
const openaiOpts = options.providerOptions?.openai;
|
|
474
|
+
const effectiveEffort = openaiOpts?.reasoningEffort ?? effort;
|
|
475
|
+
const verbosity = openaiOpts?.textVerbosity ?? openaiOpts?.verbosity;
|
|
476
|
+
const reasoningSummary = openaiOpts?.reasoningSummary;
|
|
477
|
+
const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
|
|
478
|
+
const schemaType = rawSchema ? rawSchema.type : void 0;
|
|
479
|
+
if (rawSchema && !hasTools && schemaType !== "object") console.warn(`[qgrid] responseFormat.schema top-level type is "${schemaType ?? "unknown"}". OpenAI structured output requires "object". Falling back to client-side parsing.`);
|
|
480
|
+
const jsonSchema = !hasTools && rawSchema && schemaType === "object" ? JSON.stringify(rawSchema) : void 0;
|
|
481
|
+
let runContext;
|
|
482
|
+
let toolResultsPayload;
|
|
483
|
+
let logMode;
|
|
484
|
+
if (clientRun) {
|
|
485
|
+
const toolResults = extractToolResultsFromHistory(options.prompt);
|
|
486
|
+
const resultIds = new Set(toolResults.map((r) => r.callId));
|
|
487
|
+
if (clientRun.pendingToolCallIds.size > 0 && [...clientRun.pendingToolCallIds].every((id) => resultIds.has(id))) {
|
|
488
|
+
runContext = clientRun.runContext;
|
|
489
|
+
toolResultsPayload = toolResults.filter((r) => clientRun.pendingToolCallIds.has(r.callId)).map((r) => ({
|
|
490
|
+
toolCallId: r.callId,
|
|
491
|
+
output: r.result
|
|
492
|
+
}));
|
|
493
|
+
logMode = "run";
|
|
494
|
+
} else {
|
|
495
|
+
console.warn("[qgrid] pending tool results not found in prompt, clearing client run state");
|
|
496
|
+
clientRun = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (!logMode && hasTools) logMode = "run";
|
|
500
|
+
const data = await fetch(`${serverUrl}/api/qgrid/query`, {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: { "Content-Type": "application/json" },
|
|
503
|
+
body: JSON.stringify({ args: {
|
|
504
|
+
prompt,
|
|
505
|
+
model: modelId,
|
|
506
|
+
system,
|
|
507
|
+
effort: effectiveEffort,
|
|
508
|
+
...verbosity ? { verbosity } : {},
|
|
509
|
+
...reasoningSummary ? { reasoningSummary } : {},
|
|
510
|
+
...hasTools ? { tools: tools.map(toQgridTool) } : {},
|
|
511
|
+
...jsonSchema ? { jsonSchema } : {},
|
|
512
|
+
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
513
|
+
...logMode ? { logMode } : {},
|
|
514
|
+
...runContext ? { runContext } : {},
|
|
515
|
+
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
516
|
+
} }),
|
|
517
|
+
signal: options.abortSignal
|
|
518
|
+
}).then(async (res) => {
|
|
519
|
+
if (!res.ok) {
|
|
520
|
+
const text = await res.text().catch(() => "");
|
|
521
|
+
throw new Error(`qgrid ${res.status}: ${text}`);
|
|
522
|
+
}
|
|
523
|
+
return await res.json();
|
|
524
|
+
});
|
|
525
|
+
const content = [];
|
|
526
|
+
let finishReason = {
|
|
527
|
+
unified: "stop",
|
|
528
|
+
raw: "stop"
|
|
529
|
+
};
|
|
530
|
+
if (data.content) {
|
|
531
|
+
for (const item of data.content) if (item.type === "text") content.push({
|
|
532
|
+
type: "text",
|
|
533
|
+
text: item.text
|
|
534
|
+
});
|
|
535
|
+
else content.push({
|
|
536
|
+
type: "tool-call",
|
|
537
|
+
toolCallId: item.toolCallId,
|
|
538
|
+
toolName: item.toolName,
|
|
539
|
+
input: item.input
|
|
540
|
+
});
|
|
541
|
+
if (data.finishReason === "tool-calls") finishReason = {
|
|
542
|
+
unified: "tool-calls",
|
|
543
|
+
raw: "tool_call"
|
|
544
|
+
};
|
|
545
|
+
} else content.push({
|
|
546
|
+
type: "text",
|
|
547
|
+
text: data.text
|
|
548
|
+
});
|
|
549
|
+
if (data.runContext && finishReason.unified === "tool-calls") clientRun = {
|
|
550
|
+
runContext: data.runContext,
|
|
551
|
+
pendingToolCallIds: new Set(content.filter((c) => c.type === "tool-call").map((c) => c.toolCallId))
|
|
552
|
+
};
|
|
553
|
+
else clientRun = null;
|
|
554
|
+
return {
|
|
555
|
+
content,
|
|
556
|
+
finishReason,
|
|
557
|
+
usage: {
|
|
558
|
+
inputTokens: {
|
|
559
|
+
total: data.usage.input_tokens,
|
|
560
|
+
noCache: data.usage.input_tokens - data.usage.cache_read_input_tokens,
|
|
561
|
+
cacheRead: data.usage.cache_read_input_tokens,
|
|
562
|
+
cacheWrite: data.usage.cache_creation_input_tokens
|
|
563
|
+
},
|
|
564
|
+
outputTokens: {
|
|
565
|
+
total: data.usage.output_tokens,
|
|
566
|
+
text: data.usage.output_tokens,
|
|
567
|
+
reasoning: void 0
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
warnings: [],
|
|
571
|
+
providerMetadata: { qgrid: {
|
|
572
|
+
model: data.model,
|
|
573
|
+
tokenName: data.tokenName ?? null,
|
|
574
|
+
durationMs: data.durationMs,
|
|
575
|
+
costUsd: data.costUsd
|
|
576
|
+
} },
|
|
577
|
+
response: { modelId: data.model }
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
async doStream(options) {
|
|
581
|
+
const tools = options.tools?.filter((t) => t.type === "function");
|
|
582
|
+
const hasTools = tools && tools.length > 0;
|
|
583
|
+
const { prompt, system, history } = extractPromptAndHistory(options.prompt);
|
|
584
|
+
const openaiOpts = options.providerOptions?.openai;
|
|
585
|
+
const effectiveEffort = openaiOpts?.reasoningEffort ?? effort;
|
|
586
|
+
const verbosity = openaiOpts?.textVerbosity ?? openaiOpts?.verbosity;
|
|
587
|
+
const reasoningSummary = openaiOpts?.reasoningSummary;
|
|
588
|
+
const rawSchema = options.responseFormat?.type === "json" ? options.responseFormat.schema : void 0;
|
|
589
|
+
const jsonSchema = !hasTools && rawSchema && rawSchema.type === "object" ? JSON.stringify(rawSchema) : void 0;
|
|
590
|
+
let runContext;
|
|
591
|
+
let toolResultsPayload;
|
|
592
|
+
if (clientRun) {
|
|
593
|
+
const toolResults = extractToolResultsFromHistory(options.prompt);
|
|
594
|
+
const resultIds = new Set(toolResults.map((r) => r.callId));
|
|
595
|
+
if (clientRun.pendingToolCallIds.size > 0 && [...clientRun.pendingToolCallIds].every((id) => resultIds.has(id))) {
|
|
596
|
+
runContext = clientRun.runContext;
|
|
597
|
+
toolResultsPayload = toolResults.filter((r) => clientRun.pendingToolCallIds.has(r.callId)).map((r) => ({
|
|
598
|
+
toolCallId: r.callId,
|
|
599
|
+
output: r.result
|
|
600
|
+
}));
|
|
601
|
+
} else {
|
|
602
|
+
console.warn("[qgrid] pending tool results not found in prompt, clearing client run state");
|
|
603
|
+
clientRun = null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const prepRes = await fetch(`${serverUrl}/api/qgrid/prepareStream`, {
|
|
607
|
+
method: "POST",
|
|
608
|
+
headers: { "Content-Type": "application/json" },
|
|
609
|
+
body: JSON.stringify({ args: {
|
|
610
|
+
prompt,
|
|
611
|
+
model: modelId,
|
|
612
|
+
system,
|
|
613
|
+
effort: effectiveEffort,
|
|
614
|
+
...verbosity ? { verbosity } : {},
|
|
615
|
+
...reasoningSummary ? { reasoningSummary } : {},
|
|
616
|
+
...hasTools ? { tools: tools.map(toQgridTool) } : {},
|
|
617
|
+
...jsonSchema ? { jsonSchema } : {},
|
|
618
|
+
...history.length > 0 ? { history: JSON.stringify(history) } : {},
|
|
619
|
+
logMode: "run",
|
|
620
|
+
...runContext ? { runContext } : {},
|
|
621
|
+
...toolResultsPayload ? { toolResults: toolResultsPayload } : {}
|
|
622
|
+
} }),
|
|
623
|
+
signal: options.abortSignal
|
|
624
|
+
});
|
|
625
|
+
if (!prepRes.ok) {
|
|
626
|
+
const text = await prepRes.text().catch(() => "");
|
|
627
|
+
throw new Error(`qgrid prepareStream ${prepRes.status}: ${text}`);
|
|
628
|
+
}
|
|
629
|
+
const { streamId } = await prepRes.json();
|
|
630
|
+
const streamRes = await fetch(`${serverUrl}/api/qgrid/queryStream?streamId=${streamId}`, { signal: options.abortSignal });
|
|
631
|
+
if (!streamRes.ok || !streamRes.body) {
|
|
632
|
+
const text = await streamRes.text().catch(() => "");
|
|
633
|
+
throw new Error(`qgrid stream ${streamRes.status}: ${text}`);
|
|
634
|
+
}
|
|
635
|
+
const textId = `text_${Math.random().toString(36).slice(2, 10)}`;
|
|
636
|
+
let textStarted = false;
|
|
637
|
+
let deltaTextEmitted = false;
|
|
638
|
+
return { stream: new ReadableStream({ async start(controller) {
|
|
639
|
+
try {
|
|
640
|
+
let streamCompleted = false;
|
|
641
|
+
for await (const event of parseSSE(streamRes.body)) if (event.type === "delta") {
|
|
642
|
+
if (!hasTools) {
|
|
643
|
+
if (!textStarted) {
|
|
644
|
+
controller.enqueue({
|
|
645
|
+
type: "text-start",
|
|
646
|
+
id: textId
|
|
647
|
+
});
|
|
648
|
+
textStarted = true;
|
|
649
|
+
}
|
|
650
|
+
controller.enqueue({
|
|
651
|
+
type: "text-delta",
|
|
652
|
+
id: textId,
|
|
653
|
+
delta: event.data.text
|
|
654
|
+
});
|
|
655
|
+
deltaTextEmitted = true;
|
|
656
|
+
}
|
|
657
|
+
} else if (event.type === "done") {
|
|
658
|
+
if (textStarted) {
|
|
659
|
+
controller.enqueue({
|
|
660
|
+
type: "text-end",
|
|
661
|
+
id: textId
|
|
662
|
+
});
|
|
663
|
+
textStarted = false;
|
|
664
|
+
}
|
|
665
|
+
const done = event.data;
|
|
666
|
+
if (done.runContext && done.finishReason === "tool-calls") clientRun = {
|
|
667
|
+
runContext: done.runContext,
|
|
668
|
+
pendingToolCallIds: new Set((done.content ?? []).filter((c) => c.type === "tool-call").map((c) => c.toolCallId))
|
|
669
|
+
};
|
|
670
|
+
else clientRun = null;
|
|
671
|
+
if (done.content) {
|
|
672
|
+
for (const item of done.content) if (item.type === "text" && !deltaTextEmitted) {
|
|
673
|
+
const tid = `text_${Math.random().toString(36).slice(2, 10)}`;
|
|
674
|
+
controller.enqueue({
|
|
675
|
+
type: "text-start",
|
|
676
|
+
id: tid
|
|
677
|
+
});
|
|
678
|
+
controller.enqueue({
|
|
679
|
+
type: "text-delta",
|
|
680
|
+
id: tid,
|
|
681
|
+
delta: item.text
|
|
682
|
+
});
|
|
683
|
+
controller.enqueue({
|
|
684
|
+
type: "text-end",
|
|
685
|
+
id: tid
|
|
686
|
+
});
|
|
687
|
+
} else if (item.type === "tool-call") {
|
|
688
|
+
controller.enqueue({
|
|
689
|
+
type: "tool-input-start",
|
|
690
|
+
id: item.toolCallId,
|
|
691
|
+
toolName: item.toolName
|
|
692
|
+
});
|
|
693
|
+
controller.enqueue({
|
|
694
|
+
type: "tool-input-delta",
|
|
695
|
+
id: item.toolCallId,
|
|
696
|
+
delta: item.input
|
|
697
|
+
});
|
|
698
|
+
controller.enqueue({
|
|
699
|
+
type: "tool-input-end",
|
|
700
|
+
id: item.toolCallId
|
|
701
|
+
});
|
|
702
|
+
controller.enqueue({
|
|
703
|
+
type: "tool-call",
|
|
704
|
+
toolCallId: item.toolCallId,
|
|
705
|
+
toolName: item.toolName,
|
|
706
|
+
input: item.input
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
controller.enqueue({
|
|
711
|
+
type: "finish",
|
|
712
|
+
finishReason: done.finishReason === "tool-calls" ? {
|
|
713
|
+
unified: "tool-calls",
|
|
714
|
+
raw: "tool_call"
|
|
715
|
+
} : {
|
|
716
|
+
unified: "stop",
|
|
717
|
+
raw: "stop"
|
|
718
|
+
},
|
|
719
|
+
usage: {
|
|
720
|
+
inputTokens: {
|
|
721
|
+
total: done.usage.input_tokens,
|
|
722
|
+
noCache: done.usage.input_tokens - done.usage.cache_read_input_tokens,
|
|
723
|
+
cacheRead: done.usage.cache_read_input_tokens,
|
|
724
|
+
cacheWrite: done.usage.cache_creation_input_tokens
|
|
725
|
+
},
|
|
726
|
+
outputTokens: {
|
|
727
|
+
total: done.usage.output_tokens,
|
|
728
|
+
text: done.usage.output_tokens,
|
|
729
|
+
reasoning: void 0
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
streamCompleted = true;
|
|
734
|
+
controller.close();
|
|
735
|
+
return;
|
|
736
|
+
} else if (event.type === "error") {
|
|
737
|
+
streamCompleted = true;
|
|
738
|
+
controller.error(new Error(event.data.message));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (!streamCompleted) controller.error(/* @__PURE__ */ new Error("qgrid stream ended unexpectedly"));
|
|
742
|
+
} catch (e) {
|
|
743
|
+
controller.error(e);
|
|
744
|
+
}
|
|
745
|
+
} }) };
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
750
|
+
export { createQgridLogger, qgrid as default, qgrid };
|