@constructive-io/graphql-server 4.32.0 → 4.33.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/esm/middleware/llm-api.js +96 -83
- package/middleware/llm-api.js +96 -86
- package/package.json +4 -4
|
@@ -23,39 +23,10 @@
|
|
|
23
23
|
import express, { Router } from 'express';
|
|
24
24
|
import { Logger } from '@pgpmjs/logger';
|
|
25
25
|
import { getPgPool } from 'pg-cache';
|
|
26
|
-
import
|
|
26
|
+
import { OllamaAdapter } from '@agentic-kit/ollama';
|
|
27
27
|
import { ModuleConfigCache } from 'graphile-cache';
|
|
28
28
|
import { getLlmEnvOptions, getAgentDiscovery, getLlmBillingConfig, } from 'graphile-llm';
|
|
29
29
|
const log = new Logger('llm-api');
|
|
30
|
-
/**
|
|
31
|
-
* Placeholder: replace with actual provider token counts once generateWithUsage() is approved.
|
|
32
|
-
* Estimates ~4 chars per token for English text.
|
|
33
|
-
*/
|
|
34
|
-
function placeholderAmountTokens(text) {
|
|
35
|
-
return Math.ceil(text.length / 4);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Call generate() and estimate token counts from text length.
|
|
39
|
-
* When a provider-native token counting API is approved, swap the
|
|
40
|
-
* estimation logic here without changing call sites.
|
|
41
|
-
*/
|
|
42
|
-
async function callWithUsage(client, input, onChunk) {
|
|
43
|
-
const promptText = input.messages.map((m) => m.content).join(' ');
|
|
44
|
-
let content;
|
|
45
|
-
if (onChunk || input.stream) {
|
|
46
|
-
await client.generate(input, onChunk);
|
|
47
|
-
content = '';
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
content = await client.generate(input);
|
|
51
|
-
}
|
|
52
|
-
const inputTokens = placeholderAmountTokens(promptText);
|
|
53
|
-
const outputTokens = placeholderAmountTokens(content);
|
|
54
|
-
return {
|
|
55
|
-
content,
|
|
56
|
-
usage: { input: inputTokens, output: outputTokens, totalTokens: inputTokens + outputTokens },
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
30
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
60
31
|
function getPgSettings(req) {
|
|
61
32
|
const settings = {};
|
|
@@ -90,12 +61,13 @@ async function withRlsClient(pool, pgSettings, fn) {
|
|
|
90
61
|
client.release();
|
|
91
62
|
}
|
|
92
63
|
}
|
|
93
|
-
function
|
|
64
|
+
function resolveOllamaAdapter() {
|
|
94
65
|
const { chat } = getLlmEnvOptions();
|
|
95
66
|
if (chat.provider === 'ollama') {
|
|
96
67
|
return {
|
|
97
|
-
|
|
68
|
+
adapter: new OllamaAdapter(chat.baseUrl),
|
|
98
69
|
model: chat.model,
|
|
70
|
+
baseUrl: chat.baseUrl,
|
|
99
71
|
};
|
|
100
72
|
}
|
|
101
73
|
return null;
|
|
@@ -280,7 +252,7 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
280
252
|
? await resolveBilling(pool, pgSettings, databaseId)
|
|
281
253
|
: null;
|
|
282
254
|
const inferenceLog = await getInferenceLogInfo(pool, dbname);
|
|
283
|
-
const ollama =
|
|
255
|
+
const ollama = resolveOllamaAdapter();
|
|
284
256
|
if (!ollama) {
|
|
285
257
|
res.status(503).json({ error: 'No LLM provider configured' });
|
|
286
258
|
return;
|
|
@@ -343,7 +315,7 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
343
315
|
const shouldStream = body.stream !== false;
|
|
344
316
|
const startTime = Date.now();
|
|
345
317
|
if (shouldStream) {
|
|
346
|
-
// ── SSE Streaming
|
|
318
|
+
// ── SSE Streaming via OllamaAdapter ─────────────────────────────────
|
|
347
319
|
res.writeHead(200, {
|
|
348
320
|
'Content-Type': 'text/event-stream',
|
|
349
321
|
'Cache-Control': 'no-cache',
|
|
@@ -352,32 +324,49 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
352
324
|
});
|
|
353
325
|
const messageId = `msg_${Date.now()}`;
|
|
354
326
|
try {
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
327
|
+
const systemMsg = llmMessages.find(m => m.role === 'system');
|
|
328
|
+
const nonSystem = llmMessages.filter(m => m.role !== 'system');
|
|
329
|
+
const modelDesc = ollama.adapter.createModel(model, {
|
|
330
|
+
maxOutputTokens: undefined,
|
|
331
|
+
});
|
|
332
|
+
const context = {
|
|
333
|
+
systemPrompt: systemMsg?.content,
|
|
334
|
+
messages: nonSystem.map((m) => ({
|
|
335
|
+
role: m.role,
|
|
336
|
+
content: m.content,
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
})),
|
|
339
|
+
};
|
|
340
|
+
const stream = ollama.adapter.stream(modelDesc, context, {
|
|
360
341
|
temperature: body.temperature,
|
|
361
|
-
}, (chunk) => {
|
|
362
|
-
streamedContent += chunk;
|
|
363
|
-
const event = {
|
|
364
|
-
id: messageId,
|
|
365
|
-
choices: [{
|
|
366
|
-
index: 0,
|
|
367
|
-
delta: { content: chunk, role: 'assistant' },
|
|
368
|
-
finish_reason: null,
|
|
369
|
-
}],
|
|
370
|
-
model,
|
|
371
|
-
};
|
|
372
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
373
342
|
});
|
|
374
|
-
|
|
343
|
+
let streamedContent = '';
|
|
344
|
+
for await (const event of stream) {
|
|
345
|
+
if (event.type === 'text_delta') {
|
|
346
|
+
streamedContent += event.delta;
|
|
347
|
+
const sseEvent = {
|
|
348
|
+
id: messageId,
|
|
349
|
+
choices: [{
|
|
350
|
+
index: 0,
|
|
351
|
+
delta: { content: event.delta, role: 'assistant' },
|
|
352
|
+
finish_reason: null,
|
|
353
|
+
}],
|
|
354
|
+
model,
|
|
355
|
+
};
|
|
356
|
+
res.write(`data: ${JSON.stringify(sseEvent)}\n\n`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const result = await stream.result();
|
|
375
360
|
const content = streamedContent;
|
|
376
|
-
const promptText = llmMessages.map(m => m.content).join(' ');
|
|
377
361
|
const latencyMs = Date.now() - startTime;
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
362
|
+
const usage = {
|
|
363
|
+
input: result.usage.input,
|
|
364
|
+
output: result.usage.output,
|
|
365
|
+
reasoning: result.usage.reasoning,
|
|
366
|
+
cacheRead: result.usage.cacheRead,
|
|
367
|
+
cacheWrite: result.usage.cacheWrite,
|
|
368
|
+
totalTokens: result.usage.totalTokens,
|
|
369
|
+
};
|
|
381
370
|
// Send [DONE] marker
|
|
382
371
|
res.write('data: [DONE]\n\n');
|
|
383
372
|
res.end();
|
|
@@ -398,10 +387,12 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
398
387
|
});
|
|
399
388
|
}
|
|
400
389
|
// 7. Record billing usage (fire-and-forget)
|
|
401
|
-
if (billing && totalTokens > 0) {
|
|
402
|
-
recordUsage(pool, pgSettings, billing, entityId, meterSlug, totalTokens, {
|
|
403
|
-
input_tokens:
|
|
404
|
-
output_tokens:
|
|
390
|
+
if (billing && usage.totalTokens > 0) {
|
|
391
|
+
recordUsage(pool, pgSettings, billing, entityId, meterSlug, usage.totalTokens, {
|
|
392
|
+
input_tokens: usage.input,
|
|
393
|
+
output_tokens: usage.output,
|
|
394
|
+
cache_read_tokens: usage.cacheRead,
|
|
395
|
+
cache_write_tokens: usage.cacheWrite,
|
|
405
396
|
model,
|
|
406
397
|
latency_ms: latencyMs,
|
|
407
398
|
stream: true,
|
|
@@ -416,9 +407,9 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
416
407
|
provider: 'ollama',
|
|
417
408
|
service: 'llm',
|
|
418
409
|
operation: 'chat',
|
|
419
|
-
inputTokens,
|
|
420
|
-
outputTokens,
|
|
421
|
-
totalTokens,
|
|
410
|
+
inputTokens: usage.input,
|
|
411
|
+
outputTokens: usage.output,
|
|
412
|
+
totalTokens: usage.totalTokens,
|
|
422
413
|
latencyMs,
|
|
423
414
|
status: 'ok',
|
|
424
415
|
}).catch(() => { });
|
|
@@ -433,17 +424,37 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
433
424
|
}
|
|
434
425
|
}
|
|
435
426
|
else {
|
|
436
|
-
// ── Non-streaming (batch)
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
427
|
+
// ── Non-streaming (batch) via OllamaAdapter ─────────────────────────
|
|
428
|
+
const systemMsg = llmMessages.find(m => m.role === 'system');
|
|
429
|
+
const nonSystem = llmMessages.filter(m => m.role !== 'system');
|
|
430
|
+
const modelDesc = ollama.adapter.createModel(model, {
|
|
431
|
+
maxOutputTokens: undefined,
|
|
432
|
+
});
|
|
433
|
+
const context = {
|
|
434
|
+
systemPrompt: systemMsg?.content,
|
|
435
|
+
messages: nonSystem.map((m) => ({
|
|
436
|
+
role: m.role,
|
|
437
|
+
content: m.content,
|
|
438
|
+
timestamp: Date.now(),
|
|
439
|
+
})),
|
|
440
|
+
};
|
|
441
|
+
const stream = ollama.adapter.stream(modelDesc, context, {
|
|
441
442
|
temperature: body.temperature,
|
|
442
443
|
});
|
|
444
|
+
const result = await stream.result();
|
|
445
|
+
const content = result.content
|
|
446
|
+
.filter((block) => block.type === 'text')
|
|
447
|
+
.map((block) => block.text)
|
|
448
|
+
.join('');
|
|
443
449
|
const latencyMs = Date.now() - startTime;
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
const usage = {
|
|
451
|
+
input: result.usage.input,
|
|
452
|
+
output: result.usage.output,
|
|
453
|
+
reasoning: result.usage.reasoning,
|
|
454
|
+
cacheRead: result.usage.cacheRead,
|
|
455
|
+
cacheWrite: result.usage.cacheWrite,
|
|
456
|
+
totalTokens: result.usage.totalTokens,
|
|
457
|
+
};
|
|
447
458
|
// Persist assistant message with model
|
|
448
459
|
await withRlsClient(pool, pgSettings, async (client) => {
|
|
449
460
|
await client.query(`INSERT INTO "${msgTable.schemaName}"."${msgTable.tableName}"
|
|
@@ -452,15 +463,17 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
452
463
|
threadId,
|
|
453
464
|
userId,
|
|
454
465
|
'assistant',
|
|
455
|
-
JSON.stringify([{ type: 'text', text:
|
|
466
|
+
JSON.stringify([{ type: 'text', text: content }]),
|
|
456
467
|
model,
|
|
457
468
|
]);
|
|
458
469
|
});
|
|
459
470
|
// Record billing usage
|
|
460
|
-
if (billing && totalTokens > 0) {
|
|
461
|
-
recordUsage(pool, pgSettings, billing, entityId, meterSlug, totalTokens, {
|
|
462
|
-
input_tokens:
|
|
463
|
-
output_tokens:
|
|
471
|
+
if (billing && usage.totalTokens > 0) {
|
|
472
|
+
recordUsage(pool, pgSettings, billing, entityId, meterSlug, usage.totalTokens, {
|
|
473
|
+
input_tokens: usage.input,
|
|
474
|
+
output_tokens: usage.output,
|
|
475
|
+
cache_read_tokens: usage.cacheRead,
|
|
476
|
+
cache_write_tokens: usage.cacheWrite,
|
|
464
477
|
model,
|
|
465
478
|
latency_ms: latencyMs,
|
|
466
479
|
stream: false,
|
|
@@ -475,9 +488,9 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
475
488
|
provider: 'ollama',
|
|
476
489
|
service: 'llm',
|
|
477
490
|
operation: 'chat',
|
|
478
|
-
inputTokens,
|
|
479
|
-
outputTokens,
|
|
480
|
-
totalTokens,
|
|
491
|
+
inputTokens: usage.input,
|
|
492
|
+
outputTokens: usage.output,
|
|
493
|
+
totalTokens: usage.totalTokens,
|
|
481
494
|
latencyMs,
|
|
482
495
|
status: 'ok',
|
|
483
496
|
}).catch(() => { });
|
|
@@ -486,14 +499,14 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
486
499
|
id: `msg_${Date.now()}`,
|
|
487
500
|
choices: [{
|
|
488
501
|
index: 0,
|
|
489
|
-
message: { role: 'assistant', content
|
|
502
|
+
message: { role: 'assistant', content },
|
|
490
503
|
finish_reason: 'stop',
|
|
491
504
|
}],
|
|
492
505
|
model,
|
|
493
506
|
usage: {
|
|
494
|
-
prompt_tokens:
|
|
495
|
-
completion_tokens:
|
|
496
|
-
total_tokens: totalTokens,
|
|
507
|
+
prompt_tokens: usage.input,
|
|
508
|
+
completion_tokens: usage.output,
|
|
509
|
+
total_tokens: usage.totalTokens,
|
|
497
510
|
},
|
|
498
511
|
});
|
|
499
512
|
}
|
package/middleware/llm-api.js
CHANGED
|
@@ -54,47 +54,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
54
54
|
return result;
|
|
55
55
|
};
|
|
56
56
|
})();
|
|
57
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
58
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
59
|
-
};
|
|
60
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
58
|
exports.createLlmApiRouter = createLlmApiRouter;
|
|
62
59
|
const express_1 = __importStar(require("express"));
|
|
63
60
|
const logger_1 = require("@pgpmjs/logger");
|
|
64
61
|
const pg_cache_1 = require("pg-cache");
|
|
65
|
-
const ollama_1 =
|
|
62
|
+
const ollama_1 = require("@agentic-kit/ollama");
|
|
66
63
|
const graphile_cache_1 = require("graphile-cache");
|
|
67
64
|
const graphile_llm_1 = require("graphile-llm");
|
|
68
65
|
const log = new logger_1.Logger('llm-api');
|
|
69
|
-
/**
|
|
70
|
-
* Placeholder: replace with actual provider token counts once generateWithUsage() is approved.
|
|
71
|
-
* Estimates ~4 chars per token for English text.
|
|
72
|
-
*/
|
|
73
|
-
function placeholderAmountTokens(text) {
|
|
74
|
-
return Math.ceil(text.length / 4);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Call generate() and estimate token counts from text length.
|
|
78
|
-
* When a provider-native token counting API is approved, swap the
|
|
79
|
-
* estimation logic here without changing call sites.
|
|
80
|
-
*/
|
|
81
|
-
async function callWithUsage(client, input, onChunk) {
|
|
82
|
-
const promptText = input.messages.map((m) => m.content).join(' ');
|
|
83
|
-
let content;
|
|
84
|
-
if (onChunk || input.stream) {
|
|
85
|
-
await client.generate(input, onChunk);
|
|
86
|
-
content = '';
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
content = await client.generate(input);
|
|
90
|
-
}
|
|
91
|
-
const inputTokens = placeholderAmountTokens(promptText);
|
|
92
|
-
const outputTokens = placeholderAmountTokens(content);
|
|
93
|
-
return {
|
|
94
|
-
content,
|
|
95
|
-
usage: { input: inputTokens, output: outputTokens, totalTokens: inputTokens + outputTokens },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
66
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
99
67
|
function getPgSettings(req) {
|
|
100
68
|
const settings = {};
|
|
@@ -129,12 +97,13 @@ async function withRlsClient(pool, pgSettings, fn) {
|
|
|
129
97
|
client.release();
|
|
130
98
|
}
|
|
131
99
|
}
|
|
132
|
-
function
|
|
100
|
+
function resolveOllamaAdapter() {
|
|
133
101
|
const { chat } = (0, graphile_llm_1.getLlmEnvOptions)();
|
|
134
102
|
if (chat.provider === 'ollama') {
|
|
135
103
|
return {
|
|
136
|
-
|
|
104
|
+
adapter: new ollama_1.OllamaAdapter(chat.baseUrl),
|
|
137
105
|
model: chat.model,
|
|
106
|
+
baseUrl: chat.baseUrl,
|
|
138
107
|
};
|
|
139
108
|
}
|
|
140
109
|
return null;
|
|
@@ -319,7 +288,7 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
319
288
|
? await resolveBilling(pool, pgSettings, databaseId)
|
|
320
289
|
: null;
|
|
321
290
|
const inferenceLog = await getInferenceLogInfo(pool, dbname);
|
|
322
|
-
const ollama =
|
|
291
|
+
const ollama = resolveOllamaAdapter();
|
|
323
292
|
if (!ollama) {
|
|
324
293
|
res.status(503).json({ error: 'No LLM provider configured' });
|
|
325
294
|
return;
|
|
@@ -382,7 +351,7 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
382
351
|
const shouldStream = body.stream !== false;
|
|
383
352
|
const startTime = Date.now();
|
|
384
353
|
if (shouldStream) {
|
|
385
|
-
// ── SSE Streaming
|
|
354
|
+
// ── SSE Streaming via OllamaAdapter ─────────────────────────────────
|
|
386
355
|
res.writeHead(200, {
|
|
387
356
|
'Content-Type': 'text/event-stream',
|
|
388
357
|
'Cache-Control': 'no-cache',
|
|
@@ -391,32 +360,49 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
391
360
|
});
|
|
392
361
|
const messageId = `msg_${Date.now()}`;
|
|
393
362
|
try {
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
363
|
+
const systemMsg = llmMessages.find(m => m.role === 'system');
|
|
364
|
+
const nonSystem = llmMessages.filter(m => m.role !== 'system');
|
|
365
|
+
const modelDesc = ollama.adapter.createModel(model, {
|
|
366
|
+
maxOutputTokens: undefined,
|
|
367
|
+
});
|
|
368
|
+
const context = {
|
|
369
|
+
systemPrompt: systemMsg?.content,
|
|
370
|
+
messages: nonSystem.map((m) => ({
|
|
371
|
+
role: m.role,
|
|
372
|
+
content: m.content,
|
|
373
|
+
timestamp: Date.now(),
|
|
374
|
+
})),
|
|
375
|
+
};
|
|
376
|
+
const stream = ollama.adapter.stream(modelDesc, context, {
|
|
399
377
|
temperature: body.temperature,
|
|
400
|
-
}, (chunk) => {
|
|
401
|
-
streamedContent += chunk;
|
|
402
|
-
const event = {
|
|
403
|
-
id: messageId,
|
|
404
|
-
choices: [{
|
|
405
|
-
index: 0,
|
|
406
|
-
delta: { content: chunk, role: 'assistant' },
|
|
407
|
-
finish_reason: null,
|
|
408
|
-
}],
|
|
409
|
-
model,
|
|
410
|
-
};
|
|
411
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
412
378
|
});
|
|
413
|
-
|
|
379
|
+
let streamedContent = '';
|
|
380
|
+
for await (const event of stream) {
|
|
381
|
+
if (event.type === 'text_delta') {
|
|
382
|
+
streamedContent += event.delta;
|
|
383
|
+
const sseEvent = {
|
|
384
|
+
id: messageId,
|
|
385
|
+
choices: [{
|
|
386
|
+
index: 0,
|
|
387
|
+
delta: { content: event.delta, role: 'assistant' },
|
|
388
|
+
finish_reason: null,
|
|
389
|
+
}],
|
|
390
|
+
model,
|
|
391
|
+
};
|
|
392
|
+
res.write(`data: ${JSON.stringify(sseEvent)}\n\n`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const result = await stream.result();
|
|
414
396
|
const content = streamedContent;
|
|
415
|
-
const promptText = llmMessages.map(m => m.content).join(' ');
|
|
416
397
|
const latencyMs = Date.now() - startTime;
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
398
|
+
const usage = {
|
|
399
|
+
input: result.usage.input,
|
|
400
|
+
output: result.usage.output,
|
|
401
|
+
reasoning: result.usage.reasoning,
|
|
402
|
+
cacheRead: result.usage.cacheRead,
|
|
403
|
+
cacheWrite: result.usage.cacheWrite,
|
|
404
|
+
totalTokens: result.usage.totalTokens,
|
|
405
|
+
};
|
|
420
406
|
// Send [DONE] marker
|
|
421
407
|
res.write('data: [DONE]\n\n');
|
|
422
408
|
res.end();
|
|
@@ -437,10 +423,12 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
437
423
|
});
|
|
438
424
|
}
|
|
439
425
|
// 7. Record billing usage (fire-and-forget)
|
|
440
|
-
if (billing && totalTokens > 0) {
|
|
441
|
-
recordUsage(pool, pgSettings, billing, entityId, meterSlug, totalTokens, {
|
|
442
|
-
input_tokens:
|
|
443
|
-
output_tokens:
|
|
426
|
+
if (billing && usage.totalTokens > 0) {
|
|
427
|
+
recordUsage(pool, pgSettings, billing, entityId, meterSlug, usage.totalTokens, {
|
|
428
|
+
input_tokens: usage.input,
|
|
429
|
+
output_tokens: usage.output,
|
|
430
|
+
cache_read_tokens: usage.cacheRead,
|
|
431
|
+
cache_write_tokens: usage.cacheWrite,
|
|
444
432
|
model,
|
|
445
433
|
latency_ms: latencyMs,
|
|
446
434
|
stream: true,
|
|
@@ -455,9 +443,9 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
455
443
|
provider: 'ollama',
|
|
456
444
|
service: 'llm',
|
|
457
445
|
operation: 'chat',
|
|
458
|
-
inputTokens,
|
|
459
|
-
outputTokens,
|
|
460
|
-
totalTokens,
|
|
446
|
+
inputTokens: usage.input,
|
|
447
|
+
outputTokens: usage.output,
|
|
448
|
+
totalTokens: usage.totalTokens,
|
|
461
449
|
latencyMs,
|
|
462
450
|
status: 'ok',
|
|
463
451
|
}).catch(() => { });
|
|
@@ -472,17 +460,37 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
472
460
|
}
|
|
473
461
|
}
|
|
474
462
|
else {
|
|
475
|
-
// ── Non-streaming (batch)
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
463
|
+
// ── Non-streaming (batch) via OllamaAdapter ─────────────────────────
|
|
464
|
+
const systemMsg = llmMessages.find(m => m.role === 'system');
|
|
465
|
+
const nonSystem = llmMessages.filter(m => m.role !== 'system');
|
|
466
|
+
const modelDesc = ollama.adapter.createModel(model, {
|
|
467
|
+
maxOutputTokens: undefined,
|
|
468
|
+
});
|
|
469
|
+
const context = {
|
|
470
|
+
systemPrompt: systemMsg?.content,
|
|
471
|
+
messages: nonSystem.map((m) => ({
|
|
472
|
+
role: m.role,
|
|
473
|
+
content: m.content,
|
|
474
|
+
timestamp: Date.now(),
|
|
475
|
+
})),
|
|
476
|
+
};
|
|
477
|
+
const stream = ollama.adapter.stream(modelDesc, context, {
|
|
480
478
|
temperature: body.temperature,
|
|
481
479
|
});
|
|
480
|
+
const result = await stream.result();
|
|
481
|
+
const content = result.content
|
|
482
|
+
.filter((block) => block.type === 'text')
|
|
483
|
+
.map((block) => block.text)
|
|
484
|
+
.join('');
|
|
482
485
|
const latencyMs = Date.now() - startTime;
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
+
const usage = {
|
|
487
|
+
input: result.usage.input,
|
|
488
|
+
output: result.usage.output,
|
|
489
|
+
reasoning: result.usage.reasoning,
|
|
490
|
+
cacheRead: result.usage.cacheRead,
|
|
491
|
+
cacheWrite: result.usage.cacheWrite,
|
|
492
|
+
totalTokens: result.usage.totalTokens,
|
|
493
|
+
};
|
|
486
494
|
// Persist assistant message with model
|
|
487
495
|
await withRlsClient(pool, pgSettings, async (client) => {
|
|
488
496
|
await client.query(`INSERT INTO "${msgTable.schemaName}"."${msgTable.tableName}"
|
|
@@ -491,15 +499,17 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
491
499
|
threadId,
|
|
492
500
|
userId,
|
|
493
501
|
'assistant',
|
|
494
|
-
JSON.stringify([{ type: 'text', text:
|
|
502
|
+
JSON.stringify([{ type: 'text', text: content }]),
|
|
495
503
|
model,
|
|
496
504
|
]);
|
|
497
505
|
});
|
|
498
506
|
// Record billing usage
|
|
499
|
-
if (billing && totalTokens > 0) {
|
|
500
|
-
recordUsage(pool, pgSettings, billing, entityId, meterSlug, totalTokens, {
|
|
501
|
-
input_tokens:
|
|
502
|
-
output_tokens:
|
|
507
|
+
if (billing && usage.totalTokens > 0) {
|
|
508
|
+
recordUsage(pool, pgSettings, billing, entityId, meterSlug, usage.totalTokens, {
|
|
509
|
+
input_tokens: usage.input,
|
|
510
|
+
output_tokens: usage.output,
|
|
511
|
+
cache_read_tokens: usage.cacheRead,
|
|
512
|
+
cache_write_tokens: usage.cacheWrite,
|
|
503
513
|
model,
|
|
504
514
|
latency_ms: latencyMs,
|
|
505
515
|
stream: false,
|
|
@@ -514,9 +524,9 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
514
524
|
provider: 'ollama',
|
|
515
525
|
service: 'llm',
|
|
516
526
|
operation: 'chat',
|
|
517
|
-
inputTokens,
|
|
518
|
-
outputTokens,
|
|
519
|
-
totalTokens,
|
|
527
|
+
inputTokens: usage.input,
|
|
528
|
+
outputTokens: usage.output,
|
|
529
|
+
totalTokens: usage.totalTokens,
|
|
520
530
|
latencyMs,
|
|
521
531
|
status: 'ok',
|
|
522
532
|
}).catch(() => { });
|
|
@@ -525,14 +535,14 @@ async function handleSendMessage(req, res, entityId) {
|
|
|
525
535
|
id: `msg_${Date.now()}`,
|
|
526
536
|
choices: [{
|
|
527
537
|
index: 0,
|
|
528
|
-
message: { role: 'assistant', content
|
|
538
|
+
message: { role: 'assistant', content },
|
|
529
539
|
finish_reason: 'stop',
|
|
530
540
|
}],
|
|
531
541
|
model,
|
|
532
542
|
usage: {
|
|
533
|
-
prompt_tokens:
|
|
534
|
-
completion_tokens:
|
|
535
|
-
total_tokens: totalTokens,
|
|
543
|
+
prompt_tokens: usage.input,
|
|
544
|
+
completion_tokens: usage.output,
|
|
545
|
+
total_tokens: usage.totalTokens,
|
|
536
546
|
},
|
|
537
547
|
});
|
|
538
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.33.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Constructive GraphQL Server",
|
|
6
6
|
"main": "index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"backend"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@agentic-kit/ollama": "^
|
|
44
|
+
"@agentic-kit/ollama": "^2.0.0",
|
|
45
45
|
"@constructive-io/csrf": "^0.14.0",
|
|
46
46
|
"@constructive-io/graphql-env": "^3.11.1",
|
|
47
47
|
"@constructive-io/graphql-types": "^3.10.1",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"graphile-build-pg": "5.0.2",
|
|
65
65
|
"graphile-cache": "^3.11.2",
|
|
66
66
|
"graphile-config": "1.0.1",
|
|
67
|
-
"graphile-llm": "^0.
|
|
67
|
+
"graphile-llm": "^0.9.0",
|
|
68
68
|
"graphile-settings": "^5.2.4",
|
|
69
69
|
"graphile-utils": "5.0.1",
|
|
70
70
|
"graphql": "16.13.0",
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"nodemon": "^3.1.14",
|
|
95
95
|
"ts-node": "^10.9.2"
|
|
96
96
|
},
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "f3ea414974306e3c0d1d68edc93b4cdd8fa6e806"
|
|
98
98
|
}
|