@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.
Files changed (66) hide show
  1. package/_specifications/BACKLOG.md +28 -0
  2. package/build/_models/interfaces/compare-data-options.interface.d.ts +27 -0
  3. package/build/_models/interfaces/compare-data-options.interface.d.ts.map +1 -0
  4. package/build/_models/interfaces/compare-data-options.interface.js +3 -0
  5. package/build/_models/interfaces/compare-data-options.interface.js.map +1 -0
  6. package/build/_models/interfaces/compare-data-result.interface.d.ts +13 -0
  7. package/build/_models/interfaces/compare-data-result.interface.d.ts.map +1 -0
  8. package/build/_models/interfaces/compare-data-result.interface.js +3 -0
  9. package/build/_models/interfaces/compare-data-result.interface.js.map +1 -0
  10. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts +14 -0
  11. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts.map +1 -0
  12. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js +3 -0
  13. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js.map +1 -0
  14. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts +50 -0
  15. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts.map +1 -0
  16. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js +3 -0
  17. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js.map +1 -0
  18. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.d.ts.map +1 -1
  19. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js +32 -0
  20. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js.map +1 -1
  21. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.d.ts.map +1 -1
  22. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js +20 -2
  23. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js.map +1 -1
  24. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +4 -1
  25. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
  26. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +28 -1
  27. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
  28. package/build/_modules/ai/_services/ai-provider.service-base.d.ts +21 -0
  29. package/build/_modules/ai/_services/ai-provider.service-base.d.ts.map +1 -1
  30. package/build/_modules/ai/_services/ai-provider.service-base.js +32 -0
  31. package/build/_modules/ai/_services/ai-provider.service-base.js.map +1 -1
  32. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts +17 -1
  33. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts.map +1 -1
  34. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js +16 -0
  35. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js.map +1 -1
  36. package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts +89 -0
  37. package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts.map +1 -0
  38. package/build/_modules/local-vector-search/_services/lvs-bm25.util.js +190 -0
  39. package/build/_modules/local-vector-search/_services/lvs-bm25.util.js.map +1 -0
  40. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts +18 -2
  41. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts.map +1 -1
  42. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js +57 -3
  43. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js.map +1 -1
  44. package/build/_services/base/data.service.d.ts +63 -0
  45. package/build/_services/base/data.service.d.ts.map +1 -1
  46. package/build/_services/base/data.service.js +189 -0
  47. package/build/_services/base/data.service.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/_models/interfaces/compare-data-options.interface.ts +27 -0
  50. package/src/_models/interfaces/compare-data-result.interface.ts +12 -0
  51. package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.ts +14 -0
  52. package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.ts +56 -0
  53. package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.spec.ts +92 -0
  54. package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.ts +38 -4
  55. package/src/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.ts +24 -5
  56. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.spec.ts +52 -0
  57. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +39 -10
  58. package/src/_modules/ai/_services/ai-provider.service-base.spec.ts +79 -0
  59. package/src/_modules/ai/_services/ai-provider.service-base.ts +41 -3
  60. package/src/_modules/local-vector-search/_enums/lvs-search-mode.enum.ts +16 -0
  61. package/src/_modules/local-vector-search/_services/lvs-bm25.util.spec.ts +159 -0
  62. package/src/_modules/local-vector-search/_services/lvs-bm25.util.ts +206 -0
  63. package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.spec.ts +135 -0
  64. package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.ts +95 -9
  65. package/src/_services/base/data.service.spec.ts +181 -0
  66. 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
- const response = await this.openai.embeddings.create({
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
- const response = await this.openai.embeddings.create({
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
- this.getMessageCreateInput({
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
- set.conversation.unshift(this.getDefaultSystemMessage(set.settings));
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
- this.getMessageCreateInput(set)
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