@futdevpro/nts-dynamo 1.15.24 → 1.15.29
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/_specifications/BACKLOG.md +28 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts +27 -0
- package/build/_models/interfaces/compare-data-options.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-options.interface.js +3 -0
- package/build/_models/interfaces/compare-data-options.interface.js.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts +13 -0
- package/build/_models/interfaces/compare-data-result.interface.d.ts.map +1 -0
- package/build/_models/interfaces/compare-data-result.interface.js +3 -0
- package/build/_models/interfaces/compare-data-result.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts +14 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts +50 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts.map +1 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js +3 -0
- package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js.map +1 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js +32 -0
- package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js +20 -2
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +4 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +28 -1
- package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts +21 -0
- package/build/_modules/ai/_services/ai-provider.service-base.d.ts.map +1 -1
- package/build/_modules/ai/_services/ai-provider.service-base.js +32 -0
- package/build/_modules/ai/_services/ai-provider.service-base.js.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts +17 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js +16 -0
- package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts +89 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js +190 -0
- package/build/_modules/local-vector-search/_services/lvs-bm25.util.js.map +1 -0
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts +18 -2
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts.map +1 -1
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js +57 -3
- package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js.map +1 -1
- package/build/_services/base/data.service.d.ts +63 -0
- package/build/_services/base/data.service.d.ts.map +1 -1
- package/build/_services/base/data.service.js +189 -0
- package/build/_services/base/data.service.js.map +1 -1
- package/package.json +1 -1
- package/src/_models/interfaces/compare-data-options.interface.ts +27 -0
- package/src/_models/interfaces/compare-data-result.interface.ts +12 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.ts +14 -0
- package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.ts +56 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.spec.ts +92 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.ts +38 -4
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.ts +24 -5
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.spec.ts +52 -0
- package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +39 -10
- package/src/_modules/ai/_services/ai-provider.service-base.spec.ts +79 -0
- package/src/_modules/ai/_services/ai-provider.service-base.ts +41 -3
- package/src/_modules/local-vector-search/_enums/lvs-search-mode.enum.ts +16 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.spec.ts +159 -0
- package/src/_modules/local-vector-search/_services/lvs-bm25.util.ts +206 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.spec.ts +135 -0
- package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.ts +95 -9
- package/src/_services/base/data.service.spec.ts +181 -0
- package/src/_services/base/data.service.ts +196 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
|
|
2
|
+
import { DyNTS_AI_CostEvent } from '../../../_models/interfaces/dynts-ai-cost-event.interface';
|
|
2
3
|
import { DyNTS_OAI_Embedding_ControlService } from './oai-embedding.control-service';
|
|
3
4
|
import { DyFM_OAI_Settings, DyFM_OAI_Model } from '@futdevpro/fsm-dynamo/ai/open-ai';
|
|
4
5
|
import { DyFM_Error } from '@futdevpro/fsm-dynamo';
|
|
@@ -236,5 +237,96 @@ describe('| DyNTS_OAI_Embedding_ControlService', () => {
|
|
|
236
237
|
expect(result.model).toBe('text-embedding-3-large');
|
|
237
238
|
});
|
|
238
239
|
});
|
|
240
|
+
|
|
241
|
+
describe('| FR-002 cost-event emit', () => {
|
|
242
|
+
|
|
243
|
+
/** Helper: lecseréli a service onCostEvent callback-jét spy-jal. */
|
|
244
|
+
function withCostEventCapture(): DyNTS_AI_CostEvent[] {
|
|
245
|
+
const received: DyNTS_AI_CostEvent[] = [];
|
|
246
|
+
(service as any).onCostEvent = (e: DyNTS_AI_CostEvent) => { received.push(e); };
|
|
247
|
+
return received;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
it('| createEmbedding emits 1 cost-event with embedding-single callType', async () => {
|
|
251
|
+
const received: DyNTS_AI_CostEvent[] = withCostEventCapture();
|
|
252
|
+
|
|
253
|
+
await service.createEmbedding({
|
|
254
|
+
text: 'hello',
|
|
255
|
+
model: DyFM_OAI_Model.textEmbedding_3Small,
|
|
256
|
+
issuer: 'test-issuer',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(received.length).toBe(1);
|
|
260
|
+
expect(received[0].callType).toBe('embedding-single');
|
|
261
|
+
expect(received[0].provider).toBe('open-ai');
|
|
262
|
+
expect(received[0].model).toBe(DyFM_OAI_Model.textEmbedding_3Small);
|
|
263
|
+
expect(received[0].tokensUsed.input).toBe(5);
|
|
264
|
+
expect(received[0].tokensUsed.total).toBe(5);
|
|
265
|
+
expect(received[0].tokensUsed.output).toBeUndefined();
|
|
266
|
+
expect(received[0].issuer).toBe('test-issuer');
|
|
267
|
+
expect(received[0].timestamp).toBeInstanceOf(Date);
|
|
268
|
+
expect(received[0].durationMs).toBeGreaterThanOrEqual(0);
|
|
269
|
+
expect(received[0].estimatedCostUsd).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('| createEmbeddings emits 1 cost-event with embedding-batch callType', async () => {
|
|
273
|
+
const received: DyNTS_AI_CostEvent[] = withCostEventCapture();
|
|
274
|
+
|
|
275
|
+
// Override-oljuk a top-level beforeEach mockját (prompt_tokens:5) hogy a batch
|
|
276
|
+
// szándékát tükrözze (2 input → magasabb token-count). A korábbi 10/10 expect
|
|
277
|
+
// assert-eket a default mock 5-tel cáfolta (BL-20260518-001).
|
|
278
|
+
mockEmbeddingsCreate.and.returnValue({
|
|
279
|
+
data: [
|
|
280
|
+
{ embedding: [0.1, 0.2, 0.3], index: 0, object: 'embedding' },
|
|
281
|
+
{ embedding: [0.4, 0.5, 0.6], index: 1, object: 'embedding' },
|
|
282
|
+
],
|
|
283
|
+
model: 'text-embedding-3-small',
|
|
284
|
+
object: 'list',
|
|
285
|
+
usage: { prompt_tokens: 10, total_tokens: 10 },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await service.createEmbeddings({
|
|
289
|
+
texts: ['a', 'b'],
|
|
290
|
+
model: DyFM_OAI_Model.textEmbedding_3Small,
|
|
291
|
+
issuer: 'batch-issuer',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(received.length).toBe(1);
|
|
295
|
+
expect(received[0].callType).toBe('embedding-batch');
|
|
296
|
+
expect(received[0].issuer).toBe('batch-issuer');
|
|
297
|
+
expect(received[0].tokensUsed.input).toBe(10);
|
|
298
|
+
expect(received[0].tokensUsed.total).toBe(10);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('| does NOT emit when the API call throws (emit only after success)', async () => {
|
|
302
|
+
const received: DyNTS_AI_CostEvent[] = withCostEventCapture();
|
|
303
|
+
mockEmbeddingsCreate.and.throwError(new Error('upstream API failed'));
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
await service.createEmbedding({
|
|
307
|
+
text: 'hello',
|
|
308
|
+
model: DyFM_OAI_Model.textEmbedding_3Small,
|
|
309
|
+
issuer: 'test-issuer',
|
|
310
|
+
});
|
|
311
|
+
fail('Should have thrown');
|
|
312
|
+
} catch {
|
|
313
|
+
// expected
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
expect(received.length).toBe(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('| stays non-breaking: with no onCostEvent the call still succeeds', async () => {
|
|
320
|
+
(service as any).onCostEvent = undefined;
|
|
321
|
+
|
|
322
|
+
const result = await service.createEmbedding({
|
|
323
|
+
text: 'hello',
|
|
324
|
+
model: DyFM_OAI_Model.textEmbedding_3Small,
|
|
325
|
+
issuer: 'test-issuer',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(result).toEqual([0.1, 0.2, 0.3]);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
239
331
|
});
|
|
240
332
|
|
|
@@ -27,10 +27,27 @@ export class DyNTS_OAI_Embedding_ControlService extends DyNTS_OAI_LLM_ServiceBas
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// FR-002 — per-call cost-event timing: a tényleges API-call latency mérve
|
|
31
|
+
const start: number = Date.now();
|
|
32
|
+
const response: CreateEmbeddingResponse = await this.openai.embeddings.create({
|
|
31
33
|
model: set.model,
|
|
32
34
|
input: set.text,
|
|
33
35
|
});
|
|
36
|
+
const durationMs: number = Date.now() - start;
|
|
37
|
+
|
|
38
|
+
// FR-002 — cost-event emit (safe, no-op ha nincs onCostEvent callback)
|
|
39
|
+
this.emitCostEvent({
|
|
40
|
+
callType: 'embedding-single',
|
|
41
|
+
provider: this.aiProvider,
|
|
42
|
+
model: String(set.model),
|
|
43
|
+
tokensUsed: {
|
|
44
|
+
input: response.usage?.prompt_tokens ?? 0,
|
|
45
|
+
total: response.usage?.total_tokens ?? 0,
|
|
46
|
+
},
|
|
47
|
+
durationMs: durationMs,
|
|
48
|
+
issuer: set.issuer,
|
|
49
|
+
timestamp: new Date(),
|
|
50
|
+
});
|
|
34
51
|
|
|
35
52
|
if (set.fullResponse) {
|
|
36
53
|
return response;
|
|
@@ -63,10 +80,27 @@ export class DyNTS_OAI_Embedding_ControlService extends DyNTS_OAI_LLM_ServiceBas
|
|
|
63
80
|
});
|
|
64
81
|
}
|
|
65
82
|
|
|
66
|
-
|
|
83
|
+
// FR-002 — per-call cost-event timing
|
|
84
|
+
const start: number = Date.now();
|
|
85
|
+
const response: CreateEmbeddingResponse = await this.openai.embeddings.create({
|
|
67
86
|
model: set.model,
|
|
68
87
|
input: set.texts,
|
|
69
88
|
});
|
|
89
|
+
const durationMs: number = Date.now() - start;
|
|
90
|
+
|
|
91
|
+
// FR-002 — cost-event emit (batch-call → 'embedding-batch')
|
|
92
|
+
this.emitCostEvent({
|
|
93
|
+
callType: 'embedding-batch',
|
|
94
|
+
provider: this.aiProvider,
|
|
95
|
+
model: String(set.model),
|
|
96
|
+
tokensUsed: {
|
|
97
|
+
input: response.usage?.prompt_tokens ?? 0,
|
|
98
|
+
total: response.usage?.total_tokens ?? 0,
|
|
99
|
+
},
|
|
100
|
+
durationMs: durationMs,
|
|
101
|
+
issuer: set.issuer,
|
|
102
|
+
timestamp: new Date(),
|
|
103
|
+
});
|
|
70
104
|
|
|
71
105
|
if (set.fullResponse) {
|
|
72
106
|
return response;
|
|
@@ -84,12 +118,12 @@ export class DyNTS_OAI_Embedding_ControlService extends DyNTS_OAI_LLM_ServiceBas
|
|
|
84
118
|
|
|
85
119
|
/**
|
|
86
120
|
* Visszaadja az embedding model információkat
|
|
87
|
-
*
|
|
121
|
+
*
|
|
88
122
|
* Returns embedding model information
|
|
89
123
|
*/
|
|
90
124
|
getEmbeddingInfo(model: string): DyFM_DAI_EmbeddingInfo {
|
|
91
125
|
const dimensions = DyFM_OAI_EmbeddingModelDimensions[model as keyof typeof DyFM_OAI_EmbeddingModelDimensions];
|
|
92
|
-
|
|
126
|
+
|
|
93
127
|
return {
|
|
94
128
|
provider: DyFM_AI_Provider.OpenAI,
|
|
95
129
|
model: model
|
|
@@ -467,12 +467,31 @@ export class DyNTS_OAI_LLMChat_ServiceBase extends DyNTS_OAI_LLM_ServiceBase imp
|
|
|
467
467
|
conversation: shortenedConversation,
|
|
468
468
|
});
|
|
469
469
|
|
|
470
|
+
// FR-002 — per-call cost-event timing
|
|
471
|
+
const callInput = this.getMessageCreateInput({
|
|
472
|
+
...set,
|
|
473
|
+
conversation: shortenedConversation,
|
|
474
|
+
});
|
|
475
|
+
const start: number = Date.now();
|
|
470
476
|
const result: ChatCompletion = await this.openai.chat.completions.create(
|
|
471
|
-
|
|
472
|
-
...set,
|
|
473
|
-
conversation: shortenedConversation,
|
|
474
|
-
})
|
|
477
|
+
callInput,
|
|
475
478
|
) as ChatCompletion;
|
|
479
|
+
const durationMs: number = Date.now() - start;
|
|
480
|
+
|
|
481
|
+
// FR-002 — cost-event emit (resolveConversation = 'llm-chat' flow alapból)
|
|
482
|
+
this.emitCostEvent({
|
|
483
|
+
callType: 'llm-chat',
|
|
484
|
+
provider: this.aiProvider,
|
|
485
|
+
model: String(callInput.model ?? this.defaultModel),
|
|
486
|
+
tokensUsed: {
|
|
487
|
+
input: result.usage?.prompt_tokens ?? 0,
|
|
488
|
+
output: result.usage?.completion_tokens ?? 0,
|
|
489
|
+
total: result.usage?.total_tokens ?? 0,
|
|
490
|
+
},
|
|
491
|
+
durationMs: durationMs,
|
|
492
|
+
issuer: set.issuer,
|
|
493
|
+
timestamp: new Date(),
|
|
494
|
+
});
|
|
476
495
|
|
|
477
496
|
if (set.getFullResponse) {
|
|
478
497
|
return result;
|
|
@@ -482,7 +501,7 @@ export class DyNTS_OAI_LLMChat_ServiceBase extends DyNTS_OAI_LLM_ServiceBase imp
|
|
|
482
501
|
} catch (error) {
|
|
483
502
|
throw new DyFM_Error({
|
|
484
503
|
...this.getDefaultErrorSettings('resolveConversation', error, set.issuer),
|
|
485
|
-
|
|
504
|
+
|
|
486
505
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OAB-RC0`,
|
|
487
506
|
});
|
|
488
507
|
}
|
|
@@ -343,6 +343,58 @@ describe('| DyNTS_OAI_LLM_ServiceBase', () => {
|
|
|
343
343
|
});
|
|
344
344
|
});
|
|
345
345
|
|
|
346
|
+
describe('| FR-002 cost-event emit', () => {
|
|
347
|
+
|
|
348
|
+
it('| should emit cost-event with llm-completion callType for single user message', async () => {
|
|
349
|
+
const received: any[] = [];
|
|
350
|
+
(service as any).onCostEvent = (e: any) => { received.push(e); };
|
|
351
|
+
|
|
352
|
+
await service.resolveMessage({
|
|
353
|
+
conversation: [{ role: DyFM_AI_MessageRole.user, content: 'Q?' }],
|
|
354
|
+
issuer: 'llm-issuer-1',
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(received.length).toBe(1);
|
|
358
|
+
expect(received[0].callType).toBe('llm-completion');
|
|
359
|
+
expect(received[0].provider).toBe('open-ai');
|
|
360
|
+
expect(received[0].model).toBe(DyFM_OAI_Model.gpt4o_mini);
|
|
361
|
+
expect(received[0].tokensUsed.input).toBe(10);
|
|
362
|
+
expect(received[0].tokensUsed.output).toBe(5);
|
|
363
|
+
expect(received[0].tokensUsed.total).toBe(15);
|
|
364
|
+
expect(received[0].issuer).toBe('llm-issuer-1');
|
|
365
|
+
expect(received[0].timestamp).toBeInstanceOf(Date);
|
|
366
|
+
expect(received[0].durationMs).toBeGreaterThanOrEqual(0);
|
|
367
|
+
expect(received[0].estimatedCostUsd).toBeUndefined();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('| should emit cost-event with llm-chat callType for multi-message conversation', async () => {
|
|
371
|
+
const received: any[] = [];
|
|
372
|
+
(service as any).onCostEvent = (e: any) => { received.push(e); };
|
|
373
|
+
|
|
374
|
+
await service.resolveMessage({
|
|
375
|
+
conversation: [
|
|
376
|
+
{ role: DyFM_AI_MessageRole.user, content: 'Q1?' },
|
|
377
|
+
{ role: DyFM_AI_MessageRole.assistant, content: 'A1.' },
|
|
378
|
+
{ role: DyFM_AI_MessageRole.user, content: 'Q2?' },
|
|
379
|
+
],
|
|
380
|
+
issuer: 'llm-issuer-chat',
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(received.length).toBe(1);
|
|
384
|
+
expect(received[0].callType).toBe('llm-chat');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('| constructor accepts onCostEvent option (non-breaking additive)', () => {
|
|
388
|
+
const cb: any = jasmine.createSpy('onCostEvent');
|
|
389
|
+
const s: DyNTS_OAI_LLM_ServiceBase = new DyNTS_OAI_LLM_ServiceBase({
|
|
390
|
+
...mockSettings,
|
|
391
|
+
onCostEvent: cb,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
expect((s as any).onCostEvent).toBe(cb);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
346
398
|
describe('| getMessageCreateInput', () => {
|
|
347
399
|
it('| should create input with default settings', () => {
|
|
348
400
|
const conversation: DyFM_AI_Message[] = [
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { DyNTS_global_settings } from '../../../../../_collections/global-settings.const';
|
|
16
16
|
import { ChatCompletion } from 'openai/resources';
|
|
17
17
|
import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions';
|
|
18
|
+
import { DyNTS_AI_CostEventCallback } from '../../../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
18
19
|
import { DyNTS_OAI_LLM_Predefined_Requests } from '../_models/interfaces/oai-llm-predefined-requests.interface';
|
|
19
20
|
import { DyNTS_OAI_global_settings } from '../_collections/oai-global-settings.const';
|
|
20
21
|
import { DyNTS_OAI_LLMDefaultPredefined_Requests } from '../_collections/oai-llm-predefined-requests.conts';
|
|
@@ -82,14 +83,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
82
83
|
predefinedRequests: DyNTS_OAI_LLM_Predefined_Requests = DyNTS_OAI_LLMDefaultPredefined_Requests;
|
|
83
84
|
|
|
84
85
|
constructor(
|
|
85
|
-
set?: DyFM_OAI_Settings
|
|
86
|
+
set?: DyFM_OAI_Settings & { onCostEvent?: DyNTS_AI_CostEventCallback }
|
|
86
87
|
) {
|
|
87
88
|
super();
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
DyNTS_global_settings.openAi_settings ??= DyNTS_OAI_global_settings;
|
|
90
|
-
|
|
91
|
+
|
|
91
92
|
this.openai = new OpenAI(
|
|
92
|
-
set?.config ??
|
|
93
|
+
set?.config ??
|
|
93
94
|
{
|
|
94
95
|
organization: DyNTS_global_settings.env_settings.openAi.organization,
|
|
95
96
|
apiKey: DyNTS_global_settings.env_settings.openAi.apiKey,
|
|
@@ -97,9 +98,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
97
98
|
}
|
|
98
99
|
);
|
|
99
100
|
|
|
100
|
-
this.defaultSettings = set?.defaultSettings ??
|
|
101
|
-
DyNTS_OAI_global_settings.defaultSettings ??
|
|
101
|
+
this.defaultSettings = set?.defaultSettings ??
|
|
102
|
+
DyNTS_OAI_global_settings.defaultSettings ??
|
|
102
103
|
new DyFM_OAI_CallSettings();
|
|
104
|
+
|
|
105
|
+
// FR-002 — opcionális per-call cost-event callback átadás a base-osztálynak
|
|
106
|
+
if (set?.onCostEvent) {
|
|
107
|
+
this.onCostEvent = set.onCostEvent;
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/**
|
|
@@ -445,13 +451,36 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
445
451
|
} */
|
|
446
452
|
): Promise<string | ChatCompletion> {
|
|
447
453
|
try {
|
|
448
|
-
|
|
449
|
-
|
|
454
|
+
// FR-002 — callType meghatározás a unshift ELŐTT (a system-message hozzáadás
|
|
455
|
+
// után már 2+ message lenne, ami a "chat" kategóriát mindig kiváltja).
|
|
456
|
+
// 1 user-message → 'llm-completion'; több → 'llm-chat'. A 'llm-tool-use'
|
|
457
|
+
// categoria a tools paraméterek bevezetésekor kerül használatba (jövőbeli FR).
|
|
458
|
+
const userMessageCount: number = set.conversation.length;
|
|
450
459
|
|
|
460
|
+
set.conversation.unshift(this.getDefaultSystemMessage(set.settings));
|
|
451
461
|
|
|
462
|
+
// FR-002 — per-call cost-event timing: a tényleges API-call latency mérve
|
|
463
|
+
const input: ChatCompletionCreateParamsBase = this.getMessageCreateInput(set);
|
|
464
|
+
const start: number = Date.now();
|
|
452
465
|
const result: ChatCompletion = await this.openai.chat.completions.create(
|
|
453
|
-
|
|
466
|
+
input,
|
|
454
467
|
) as ChatCompletion;
|
|
468
|
+
const durationMs: number = Date.now() - start;
|
|
469
|
+
|
|
470
|
+
// FR-002 — cost-event emit (safe, no-op ha nincs onCostEvent callback)
|
|
471
|
+
this.emitCostEvent({
|
|
472
|
+
callType: userMessageCount === 1 ? 'llm-completion' : 'llm-chat',
|
|
473
|
+
provider: this.aiProvider,
|
|
474
|
+
model: String(input.model ?? this.defaultModel),
|
|
475
|
+
tokensUsed: {
|
|
476
|
+
input: result.usage?.prompt_tokens ?? 0,
|
|
477
|
+
output: result.usage?.completion_tokens ?? 0,
|
|
478
|
+
total: result.usage?.total_tokens ?? 0,
|
|
479
|
+
},
|
|
480
|
+
durationMs: durationMs,
|
|
481
|
+
issuer: set.issuer,
|
|
482
|
+
timestamp: new Date(),
|
|
483
|
+
});
|
|
455
484
|
|
|
456
485
|
if (set.getFullResponse) {
|
|
457
486
|
return result;
|
|
@@ -461,7 +490,7 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
|
|
|
461
490
|
} catch (error) {
|
|
462
491
|
throw new DyFM_Error({
|
|
463
492
|
...this.getDefaultErrorSettings('resolveConversation', error, set.issuer),
|
|
464
|
-
|
|
493
|
+
|
|
465
494
|
errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OLSB-RM0`,
|
|
466
495
|
});
|
|
467
496
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
|
+
import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
|
|
3
|
+
import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
2
4
|
import { DyNTS_AI_Provider_ServiceBase } from './ai-provider.service-base';
|
|
3
5
|
import { DyFM_AI_Provider, DyFM_AI_ProviderCapabilities, DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
|
|
4
6
|
|
|
@@ -25,11 +27,35 @@ class TestProviderService extends DyNTS_AI_Provider_ServiceBase {
|
|
|
25
27
|
return true;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
/** Test-only helper: ráad egy callback-et a protected onCostEvent fielden át. */
|
|
31
|
+
public setOnCostEventForTest(cb: DyNTS_AI_CostEventCallback | undefined): void {
|
|
32
|
+
this.onCostEvent = cb;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Test-only helper: meghívja a protected emitCostEvent-et (a sub-class amúgy is hívná). */
|
|
36
|
+
public emitForTest(event: DyNTS_AI_CostEvent): void {
|
|
37
|
+
this.emitCostEvent(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
static getInstance(): TestProviderService {
|
|
29
41
|
return TestProviderService.getSingletonInstance();
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
|
|
45
|
+
/** Helper: minimal valid DyNTS_AI_CostEvent. */
|
|
46
|
+
function makeCostEvent(overrides: Partial<DyNTS_AI_CostEvent> = {}): DyNTS_AI_CostEvent {
|
|
47
|
+
return {
|
|
48
|
+
callType: 'embedding-single',
|
|
49
|
+
provider: 'openai',
|
|
50
|
+
model: 'text-embedding-3-small',
|
|
51
|
+
tokensUsed: { input: 10, total: 10 },
|
|
52
|
+
durationMs: 42,
|
|
53
|
+
issuer: 'test-issuer',
|
|
54
|
+
timestamp: new Date(),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
describe('| DyNTS_AI_Provider_ServiceBase', () => {
|
|
34
60
|
let service: TestProviderService;
|
|
35
61
|
|
|
@@ -75,5 +101,58 @@ describe('| DyNTS_AI_Provider_ServiceBase', () => {
|
|
|
75
101
|
expect(result).toBe(true);
|
|
76
102
|
});
|
|
77
103
|
});
|
|
104
|
+
|
|
105
|
+
describe('| emitCostEvent (FR-002 cost-event hook)', () => {
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
// Reset callback hogy ne szivárogjon át teszt-cases között (singleton service)
|
|
109
|
+
service.setOnCostEventForTest(undefined);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('| should be no-op when onCostEvent is undefined', () => {
|
|
113
|
+
service.setOnCostEventForTest(undefined);
|
|
114
|
+
|
|
115
|
+
expect(() => {
|
|
116
|
+
service.emitForTest(makeCostEvent());
|
|
117
|
+
}).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('| should invoke the registered callback with the event payload', () => {
|
|
121
|
+
const received: DyNTS_AI_CostEvent[] = [];
|
|
122
|
+
service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => {
|
|
123
|
+
received.push(e);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const event: DyNTS_AI_CostEvent = makeCostEvent({
|
|
127
|
+
model: 'gpt-4o-mini',
|
|
128
|
+
callType: 'llm-completion',
|
|
129
|
+
tokensUsed: { input: 100, output: 50, total: 150 },
|
|
130
|
+
});
|
|
131
|
+
service.emitForTest(event);
|
|
132
|
+
|
|
133
|
+
expect(received.length).toBe(1);
|
|
134
|
+
expect(received[0]).toEqual(event);
|
|
135
|
+
expect(received[0].tokensUsed.total).toBe(150);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('| should NOT throw when the callback itself throws (safe emit)', () => {
|
|
139
|
+
service.setOnCostEventForTest(() => {
|
|
140
|
+
throw new Error('consumer callback failure');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(() => {
|
|
144
|
+
service.emitForTest(makeCostEvent());
|
|
145
|
+
}).not.toThrow();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('| should leave estimatedCostUsd undefined by default (Dynamo has no pricing registry)', () => {
|
|
149
|
+
const received: DyNTS_AI_CostEvent[] = [];
|
|
150
|
+
service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => { received.push(e); });
|
|
151
|
+
|
|
152
|
+
service.emitForTest(makeCostEvent());
|
|
153
|
+
|
|
154
|
+
expect(received[0].estimatedCostUsd).toBeUndefined();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
78
157
|
});
|
|
79
158
|
|
|
@@ -1,29 +1,67 @@
|
|
|
1
1
|
import { DyFM_AI_Provider } from '@futdevpro/fsm-dynamo/ai';
|
|
2
2
|
import { DyFM_AI_ProviderCapabilities } from '@futdevpro/fsm-dynamo/ai';
|
|
3
3
|
import { DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
|
|
4
|
+
import { DyFM_Log } from '@futdevpro/fsm-dynamo';
|
|
5
|
+
|
|
6
|
+
import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
|
|
7
|
+
import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
|
|
4
8
|
import { DyNTS_SingletonService } from '../../../_services/base/singleton.service';
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Abstract base class for all AI providers
|
|
8
12
|
* Defines the common interface that all AI providers must implement
|
|
13
|
+
*
|
|
14
|
+
* **Cost-event hook (FR-002)**: a leszármazott concrete service-ek (OAI
|
|
15
|
+
* Embedding / LLM / Chat) a constructor-ban opcionálisan kapnak egy
|
|
16
|
+
* `onCostEvent` callback-et. Minden sikeres AI-call után `emitCostEvent`-tel
|
|
17
|
+
* hívják, ami safe (try/catch) — a callback hibája NEM akasztja meg az
|
|
18
|
+
* AI-call-t. Lásd `DyNTS_AI_CostEvent` interface.
|
|
9
19
|
*/
|
|
10
20
|
export abstract class DyNTS_AI_Provider_ServiceBase extends DyNTS_SingletonService {
|
|
11
21
|
/** The AI provider this service represents */
|
|
12
22
|
abstract readonly aiProvider: DyFM_AI_Provider;
|
|
13
|
-
|
|
23
|
+
|
|
14
24
|
/** The capabilities supported by this provider */
|
|
15
25
|
abstract readonly capabilities: DyFM_AI_ProviderCapabilities;
|
|
16
|
-
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional per-call cost-event callback (FR-002).
|
|
29
|
+
* A leszármazott service-ek minden sikeres AI-call után meghívják az
|
|
30
|
+
* `emitCostEvent` helper-en keresztül. Non-breaking: ha undefined, nincs emit.
|
|
31
|
+
*/
|
|
32
|
+
protected onCostEvent?: DyNTS_AI_CostEventCallback;
|
|
33
|
+
|
|
17
34
|
/**
|
|
18
35
|
* Initialize the provider with configuration
|
|
19
36
|
* @param config - Provider-specific configuration
|
|
20
37
|
*/
|
|
21
38
|
abstract setup(config: DyFM_AI_Config): void;
|
|
22
|
-
|
|
39
|
+
|
|
23
40
|
/**
|
|
24
41
|
* Test the connection to the AI provider
|
|
25
42
|
* @param issuer - The issuer making the request
|
|
26
43
|
* @returns Promise<boolean> - True if connection is successful
|
|
27
44
|
*/
|
|
28
45
|
abstract testConnection(issuer: string): Promise<boolean>;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Safe cost-event emit: ha van regisztrált `onCostEvent` callback, meghívja.
|
|
49
|
+
* A callback hibája NEM propagálódik — try/catch-csel safen hívjuk és
|
|
50
|
+
* `DyFM_Log.warn`-nal jelezzük. Ezzel a consumer-oldali integráció
|
|
51
|
+
* (CCAP `CT_`, telemetry, stb.) hibái nem akasztják meg az AI-call-t.
|
|
52
|
+
*/
|
|
53
|
+
protected emitCostEvent(event: DyNTS_AI_CostEvent): void {
|
|
54
|
+
if (!this.onCostEvent) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
this.onCostEvent(event);
|
|
59
|
+
} catch (err: unknown) {
|
|
60
|
+
const msg: string = err instanceof Error ? err.message : String(err);
|
|
61
|
+
DyFM_Log.warn(
|
|
62
|
+
`DyNTS AI | onCostEvent callback threw (call-type: ${event.callType},`
|
|
63
|
+
+ ` model: ${event.model}, issuer: ${event.issuer}): ${msg}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
29
67
|
}
|
|
@@ -15,5 +15,21 @@ export enum LVS_Search_Mode {
|
|
|
15
15
|
* Eredmények növekvő sorrendben (legkisebb távolság először)
|
|
16
16
|
*/
|
|
17
17
|
l2Distance = 'l2-distance',
|
|
18
|
+
/**
|
|
19
|
+
* Hybrid search: cosine similarity (vektor-fele) + BM25 (text-fele) score-merge.
|
|
20
|
+
*
|
|
21
|
+
* Hasznalat:
|
|
22
|
+
* - `searchMode: LVS_Search_Mode.hybrid`
|
|
23
|
+
* - `textSearchKey: keyof T` — KOTELEZO; melyik string property-n fut a BM25
|
|
24
|
+
* - `hybridWeight?: { vector: number; text: number }` — default { vector: 0.5, text: 0.5 }
|
|
25
|
+
*
|
|
26
|
+
* BM25 min-max normalizalva [0,1] a candidate-szettre (cosine mar 0..1), igy
|
|
27
|
+
* a `weight` direkt linearis kombinacio. Ha minden BM25 score 0 (egyetlen
|
|
28
|
+
* query term sem matchel), a hybrid effektivan cosine-only-re degradalodik.
|
|
29
|
+
*
|
|
30
|
+
* L2+hybrid kombinacio NEM tamogatott — a hybrid mindig cosine-alapu
|
|
31
|
+
* vector-half-fel mukodik.
|
|
32
|
+
*/
|
|
33
|
+
hybrid = 'hybrid',
|
|
18
34
|
}
|
|
19
35
|
|