@nestjs/ai 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +1 -1
  2. package/dist/ai.constants.d.ts +8 -0
  3. package/dist/ai.constants.js +10 -0
  4. package/dist/ai.module-definition.d.ts +2 -2
  5. package/dist/ai.module-definition.js +14 -5
  6. package/dist/ai.module.js +13 -4
  7. package/dist/decorators/index.d.ts +1 -0
  8. package/dist/decorators/index.js +1 -0
  9. package/dist/decorators/inject-embedding-model.decorator.d.ts +10 -0
  10. package/dist/decorators/inject-embedding-model.decorator.js +16 -0
  11. package/dist/index.d.ts +4 -3
  12. package/dist/index.js +2 -1
  13. package/dist/interfaces/ai-embedding.interface.d.ts +79 -0
  14. package/dist/interfaces/ai-embedding.interface.js +1 -0
  15. package/dist/interfaces/ai-module-options.interface.d.ts +31 -9
  16. package/dist/interfaces/ai-result.interface.d.ts +4 -1
  17. package/dist/interfaces/index.d.ts +1 -0
  18. package/dist/interfaces/index.js +1 -0
  19. package/dist/models/ai-embedding-models.service.d.ts +31 -0
  20. package/dist/models/ai-embedding-models.service.js +138 -0
  21. package/dist/models/ai-models.service.js +3 -4
  22. package/dist/models/model-registry.util.d.ts +6 -0
  23. package/dist/models/model-registry.util.js +21 -0
  24. package/dist/models/model-requests.d.ts +3 -0
  25. package/dist/models/model-requests.js +8 -0
  26. package/dist/runtime/embedding-call.d.ts +38 -0
  27. package/dist/runtime/embedding-call.js +120 -0
  28. package/dist/runtime/model-policy.d.ts +14 -9
  29. package/dist/runtime/model-policy.js +7 -7
  30. package/dist/services/ai.service.d.ts +14 -1
  31. package/dist/services/ai.service.js +51 -7
  32. package/dist/testing/fake-embedding-model.d.ts +57 -0
  33. package/dist/testing/fake-embedding-model.js +82 -0
  34. package/dist/testing/index.d.ts +7 -1
  35. package/dist/testing/index.js +9 -1
  36. package/package.json +17 -2
@@ -4,9 +4,17 @@
4
4
  * not configure fails with a message of its own, not with Nest's missing-provider error.
5
5
  */
6
6
  const requested = new Set();
7
+ const requestedEmbeddings = new Set();
7
8
  export function recordModelRequest(name) {
8
9
  requested.add(name);
9
10
  }
10
11
  export function requestedModelNames() {
11
12
  return [...requested];
12
13
  }
14
+ /** Embedding injection requests use a separate namespace, even when the names are identical. */
15
+ export function recordEmbeddingModelRequest(name) {
16
+ requestedEmbeddings.add(name);
17
+ }
18
+ export function requestedEmbeddingModelNames() {
19
+ return [...requestedEmbeddings];
20
+ }
@@ -0,0 +1,38 @@
1
+ import type { ResiliencePolicy } from '@nestjs/resilience';
2
+ import { type EmbedManyResult, type EmbedResult } from 'ai';
3
+ import type { AiEmbeddingCallOptions, AiEmbeddingSettings, AiEmbeddingUsageEvent } from '../interfaces/ai-embedding.interface.js';
4
+ import type { AiModuleOptions } from '../interfaces/ai-module-options.interface.js';
5
+ import type { AiEmbeddingModels, ResolvedEmbeddingModel } from '../models/ai-embedding-models.service.js';
6
+ import { type PolicyPlan } from './model-policy.js';
7
+ /** One operation's state. Clients and explicitly supplied resilience policies remain shared. */
8
+ export interface EmbeddingPlan extends PolicyPlan<ResolvedEmbeddingModel> {
9
+ model: ResolvedEmbeddingModel;
10
+ label?: string;
11
+ settings: Pick<AiEmbeddingSettings, 'headers' | 'providerOptions'> & {
12
+ maxParallelCalls: number;
13
+ };
14
+ listeners: Array<(event: AiEmbeddingUsageEvent) => void | Promise<void>>;
15
+ }
16
+ /**
17
+ * Resolves only embedding settings: language generation defaults never reach an embedding provider.
18
+ */
19
+ export declare function createEmbeddingPlan(options: AiEmbeddingCallOptions, models: AiEmbeddingModels, moduleOptions: AiModuleOptions, preset: (name: string) => ResiliencePolicy): EmbeddingPlan;
20
+ interface EmbeddingExecution<R> {
21
+ result: R;
22
+ event: AiEmbeddingUsageEvent;
23
+ attempts: number;
24
+ cost?: number;
25
+ }
26
+ /**
27
+ * The SDK splits `values` into provider requests. The policy runs around each of them, through the
28
+ * SDK's middleware, so a timeout or a retry covers one request and never repeats the others. When a
29
+ * request fails for good, its siblings are aborted, so that a failed operation does not keep
30
+ * consuming provider capacity.
31
+ */
32
+ export declare function runEmbedding(plan: EmbeddingPlan, input: {
33
+ value: string;
34
+ }): Promise<EmbeddingExecution<EmbedResult>>;
35
+ export declare function runEmbedding(plan: EmbeddingPlan, input: {
36
+ values: string[];
37
+ }): Promise<EmbeddingExecution<EmbedManyResult>>;
38
+ export {};
@@ -0,0 +1,120 @@
1
+ import { embed, embedMany, gateway, wrapEmbeddingModel, } from 'ai';
2
+ import { AiConfigurationError } from '../errors/ai.errors.js';
3
+ import { modelPolicy, runStep } from './model-policy.js';
4
+ const NEVER_ABORTED = new AbortController().signal;
5
+ const DEFAULT_MAX_PARALLEL_CALLS = 4;
6
+ /**
7
+ * Resolves only embedding settings: language generation defaults never reach an embedding provider.
8
+ */
9
+ export function createEmbeddingPlan(options, models, moduleOptions, preset) {
10
+ if (typeof options !== 'object' || options === null) {
11
+ throw new AiConfigurationError('Pass the embedding call its options: { value } or { values }, and the rest.');
12
+ }
13
+ const model = models.resolve(options.model);
14
+ const defaults = moduleOptions.embeddingDefaults ?? {};
15
+ const pick = (key) => options[key] ?? model.settings[key] ?? defaults[key];
16
+ const maxParallelCalls = pick('maxParallelCalls') ?? DEFAULT_MAX_PARALLEL_CALLS;
17
+ if (!Number.isSafeInteger(maxParallelCalls) || maxParallelCalls < 1) {
18
+ throw new AiConfigurationError('`maxParallelCalls` must be a finite positive whole number.');
19
+ }
20
+ const own = pick('resilience');
21
+ const policy = own === undefined
22
+ ? modelPolicy(model, { retry: pick('retry'), timeout: pick('timeout') }, 'ai:embedding')
23
+ : typeof own === 'string'
24
+ ? preset(own)
25
+ : own;
26
+ return {
27
+ model,
28
+ chain: [model],
29
+ signal: options.abortSignal ?? NEVER_ABORTED,
30
+ source: options.label ?? 'AiService',
31
+ fallbackIf: () => false,
32
+ policyFor: () => policy,
33
+ ...(options.label !== undefined && { label: options.label }),
34
+ settings: {
35
+ maxParallelCalls,
36
+ ...(pick('headers') !== undefined && { headers: pick('headers') }),
37
+ ...(pick('providerOptions') !== undefined && { providerOptions: pick('providerOptions') }),
38
+ },
39
+ listeners: [moduleOptions.onEmbeddingUsage, options.onEmbeddingUsage].filter((listener) => typeof listener === 'function'),
40
+ };
41
+ }
42
+ export async function runEmbedding(plan, input) {
43
+ const started = performance.now();
44
+ const many = 'values' in input;
45
+ if (many
46
+ ? !Array.isArray(input.values) || [...input.values].some((value) => typeof value !== 'string')
47
+ : typeof input.value !== 'string') {
48
+ throw new AiConfigurationError(many ? '`values` must be an array of strings.' : '`value` must be a string.');
49
+ }
50
+ const values = many ? [...input.values] : undefined;
51
+ const controller = new AbortController();
52
+ const signal = AbortSignal.any([plan.signal, controller.signal]);
53
+ let attempts = 0;
54
+ const wrapped = wrapEmbeddingModel({
55
+ model: modelObject(plan.model.model),
56
+ middleware: {
57
+ wrapEmbed: async ({ params, model }) => {
58
+ const { value, attempts: used } = await runStep({ ...plan, signal }, async (attempt) => {
59
+ attempt.signal.throwIfAborted();
60
+ return model.doEmbed({ ...params, abortSignal: attempt.signal });
61
+ });
62
+ attempts += used;
63
+ return value;
64
+ },
65
+ },
66
+ });
67
+ let result;
68
+ try {
69
+ signal.throwIfAborted();
70
+ const settings = {
71
+ model: wrapped,
72
+ headers: plan.settings.headers,
73
+ providerOptions: plan.settings.providerOptions,
74
+ abortSignal: signal,
75
+ maxRetries: 0,
76
+ };
77
+ result =
78
+ values !== undefined
79
+ ? await embedMany({
80
+ ...settings,
81
+ values,
82
+ maxParallelCalls: plan.settings.maxParallelCalls,
83
+ })
84
+ : await embed({ ...settings, value: input.value });
85
+ signal.throwIfAborted();
86
+ }
87
+ finally {
88
+ controller.abort();
89
+ }
90
+ const pricing = plan.model.pricing;
91
+ const cost = pricing && Number.isFinite(result.usage.tokens)
92
+ ? (result.usage.tokens * pricing.input) / 1_000_000
93
+ : undefined;
94
+ return {
95
+ result,
96
+ attempts,
97
+ ...(cost !== undefined && { cost }),
98
+ event: {
99
+ operation: many ? 'embedMany' : 'embed',
100
+ ...(plan.label !== undefined && { label: plan.label }),
101
+ model: plan.model.info,
102
+ inputCount: values?.length ?? 1,
103
+ usage: { tokens: result.usage.tokens },
104
+ attempts,
105
+ durationMs: Math.round(performance.now() - started),
106
+ ...(cost !== undefined && { cost }),
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * `wrapEmbeddingModel()` takes model objects. An id resolves as the SDK resolves it: with its
112
+ * default provider, the gateway unless the application set another.
113
+ */
114
+ function modelObject(model) {
115
+ if (typeof model !== 'string') {
116
+ return model;
117
+ }
118
+ const provider = (globalThis.AI_SDK_DEFAULT_PROVIDER ?? gateway);
119
+ return provider.embeddingModel(model);
120
+ }
@@ -1,8 +1,13 @@
1
1
  import { ResiliencePolicy, type RetryOptions } from '@nestjs/resilience';
2
2
  import type { ResolvedModel } from '../models/ai-models.service.js';
3
+ import type { AiModelInfo } from '../interfaces/ai-result.interface.js';
4
+ /** The identity the policy layer needs; generation and embedding registries stay separately typed. */
5
+ export interface PolicyModel {
6
+ info: AiModelInfo;
7
+ }
3
8
  /** What one attempt of a model call receives. */
4
- export interface StepAttempt {
5
- model: ResolvedModel;
9
+ export interface StepAttempt<M extends PolicyModel = ResolvedModel> {
10
+ model: M;
6
11
  /** 1-based, per model. */
7
12
  attempt: number;
8
13
  fallback: boolean;
@@ -14,14 +19,14 @@ export interface StepAttempt {
14
19
  emit(chunk: unknown): void;
15
20
  }
16
21
  /** How a call's models are retried, timed out and replaced. Internal. */
17
- export interface PolicyPlan {
18
- chain: ResolvedModel[];
22
+ export interface PolicyPlan<M extends PolicyModel = ResolvedModel> {
23
+ chain: M[];
19
24
  /** The caller's signal. */
20
25
  signal: AbortSignal;
21
26
  /** Labels the policies' events (`source`). */
22
27
  source: string;
23
28
  /** The model's own policy: an application's, or the one built from its `retry` and `timeout`. */
24
- policyFor(model: ResolvedModel): ResiliencePolicy | undefined;
29
+ policyFor(model: M): ResiliencePolicy | undefined;
25
30
  fallbackIf: (error: unknown) => boolean;
26
31
  }
27
32
  /**
@@ -31,18 +36,18 @@ export interface PolicyPlan {
31
36
  * every attempt, the caller's abort reason, or what is not a model failure (an `AiOutputError`, a circuit breaker's
32
37
  * rejection) as it is.
33
38
  */
34
- export declare function runStep<T>(plan: PolicyPlan, run: (attempt: StepAttempt) => Promise<T>, emit?: (chunk: unknown) => void): Promise<{
39
+ export declare function runStep<T, M extends PolicyModel = ResolvedModel>(plan: PolicyPlan<M>, run: (attempt: StepAttempt<M>) => Promise<T>, emit?: (chunk: unknown) => void): Promise<{
35
40
  value: T;
36
- attempt: StepAttempt;
41
+ attempt: StepAttempt<M>;
37
42
  attempts: number;
38
43
  }>;
39
44
  /**
40
45
  * The model's own policy from its settings: `RetryPolicy` (honoring the provider's `retry-after`) around
41
46
  * `TimeoutPolicy`, either one left out when it is `false`.
42
47
  */
43
- export declare function modelPolicy(model: ResolvedModel, { retry, timeout }: {
48
+ export declare function modelPolicy(model: PolicyModel, { retry, timeout }: {
44
49
  retry: RetryOptions | false | undefined;
45
50
  timeout: number | string | false | undefined;
46
- }): ResiliencePolicy | undefined;
51
+ }, namespace?: string): ResiliencePolicy | undefined;
47
52
  /** What is retried by default: what the provider marks retryable (with network failures), and timeouts. */
48
53
  export declare function isRetryable(error: unknown): boolean;
@@ -98,24 +98,24 @@ export function runStep(plan, run, emit) {
98
98
  * The model's own policy from its settings: `RetryPolicy` (honoring the provider's `retry-after`) around
99
99
  * `TimeoutPolicy`, either one left out when it is `false`.
100
100
  */
101
- export function modelPolicy(model, { retry, timeout }) {
101
+ export function modelPolicy(model, { retry, timeout }, namespace = 'ai') {
102
102
  const policies = [];
103
103
  if (retry !== false) {
104
- policies.push(retryPolicy(model, retry ?? {}));
104
+ policies.push(retryPolicy(model, retry ?? {}, namespace));
105
105
  }
106
106
  if (timeout !== false) {
107
- policies.push(new TimeoutPolicy({ timeout: toMilliseconds(timeout ?? DEFAULT_TIMEOUT, 'timeout'), name: `${nameOf(model)} timeout` }));
107
+ policies.push(new TimeoutPolicy({ timeout: toMilliseconds(timeout ?? DEFAULT_TIMEOUT, 'timeout'), name: `${nameOf(model, namespace)} timeout` }));
108
108
  }
109
109
  return policies.length > 0 ? ResiliencePolicy.wrap(...policies) : undefined;
110
110
  }
111
111
  const DEFAULT_TIMEOUT = '60s';
112
112
  const DEFAULT_BACKOFF = { delay: 1_000, factor: 2, maxDelay: 30_000 };
113
- function retryPolicy(model, retry) {
113
+ function retryPolicy(model, retry, namespace) {
114
114
  const backoff = retry.backoff;
115
115
  const maxDelay = typeof backoff === 'object' && backoff.maxDelay !== undefined ? toMilliseconds(backoff.maxDelay, 'retry.backoff.maxDelay') : DEFAULT_BACKOFF.maxDelay;
116
116
  const retryIf = retry.retryIf ?? isRetryable;
117
117
  return new RetryPolicy({
118
- name: `${nameOf(model)} retry`,
118
+ name: `${nameOf(model, namespace)} retry`,
119
119
  attempts: retry.attempts ?? 3,
120
120
  ...(retry.retryOnResult && { retryOnResult: retry.retryOnResult }),
121
121
  // A provider that asks for a longer wait than the backoff allows: on to the fallback.
@@ -185,8 +185,8 @@ function retryAfter(error) {
185
185
  const date = Date.parse(value);
186
186
  return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
187
187
  }
188
- function nameOf(model) {
189
- return `ai:${labelOf(model)}`;
188
+ function nameOf(model, namespace = 'ai') {
189
+ return `${namespace}:${labelOf(model)}`;
190
190
  }
191
191
  function labelOf(model) {
192
192
  return model.info.name ?? `${model.info.provider}:${model.info.modelId}`;
@@ -1,10 +1,12 @@
1
1
  import { ModuleRef } from '@nestjs/core';
2
2
  import type { ModelMessage } from 'ai';
3
3
  import type { AiCallOptions, AiObjectCallOptions } from '../interfaces/ai-call-options.interface.js';
4
+ import type { AiEmbedOptions, AiEmbedManyOptions, AiEmbedResult, AiEmbedManyResult } from '../interfaces/ai-embedding.interface.js';
4
5
  import type { AiModuleOptions } from '../interfaces/ai-module-options.interface.js';
5
6
  import type { AiObjectResult, AiTextResult } from '../interfaces/ai-result.interface.js';
6
7
  import type { AiSchema, InferSchemaOutput } from '../interfaces/ai-schema.interface.js';
7
8
  import { AiModels } from '../models/ai-models.service.js';
9
+ import { AiEmbeddingModels } from '../models/ai-embedding-models.service.js';
8
10
  import { type StepOutcome } from '../runtime/steps.js';
9
11
  import { AiTextStream } from '../runtime/text-stream.js';
10
12
  import { AiTools } from '../tools/ai-tools.service.js';
@@ -21,8 +23,19 @@ export declare class AiService {
21
23
  private readonly tools;
22
24
  private readonly options;
23
25
  private readonly moduleRef;
26
+ private readonly embeddingModels;
24
27
  private readonly logger;
25
- constructor(models: AiModels, tools: AiTools, options: AiModuleOptions, moduleRef: ModuleRef);
28
+ constructor(models: AiModels, tools: AiTools, options: AiModuleOptions, moduleRef: ModuleRef, embeddingModels: AiEmbeddingModels);
29
+ /**
30
+ * Embeds one string, with the embedding model's settings, Nest policies and embedding usage
31
+ * events.
32
+ */
33
+ embed(options: AiEmbedOptions): Promise<AiEmbedResult>;
34
+ /**
35
+ * Embeds strings in input order; the SDK batches requests, with bounded concurrency (default 4).
36
+ * Timeouts, retries and policies apply to each request.
37
+ */
38
+ embedMany(options: AiEmbedManyOptions): Promise<AiEmbedManyResult>;
26
39
  /** Generates text; with `tools`, calls them as the model asks, and answers when it is done. */
27
40
  generateText(options: AiCallOptions): Promise<AiTextResult>;
28
41
  /**
@@ -16,8 +16,10 @@ import { ResilienceService } from '@nestjs/resilience';
16
16
  import { AI_MODULE_OPTIONS } from '../ai.constants.js';
17
17
  import { AiConfigurationError, AiOutputError } from '../errors/ai.errors.js';
18
18
  import { AiModels } from '../models/ai-models.service.js';
19
+ import { AiEmbeddingModels } from '../models/ai-embedding-models.service.js';
19
20
  import { runLoop, usageEvent } from '../runtime/agent-loop.js';
20
21
  import { createCallPlan } from '../runtime/call-plan.js';
22
+ import { createEmbeddingPlan, runEmbedding } from '../runtime/embedding-call.js';
21
23
  import { runStep } from '../runtime/model-policy.js';
22
24
  import { generateStep } from '../runtime/steps.js';
23
25
  import { AiTextStream } from '../runtime/text-stream.js';
@@ -33,13 +35,54 @@ const NEVER_ABORTED = new AbortController().signal;
33
35
  * ```
34
36
  */
35
37
  let AiService = class AiService {
36
- constructor(models, tools, options, moduleRef) {
38
+ constructor(models, tools, options, moduleRef, embeddingModels) {
37
39
  this.models = models;
38
40
  this.tools = tools;
39
41
  this.options = options;
40
42
  this.moduleRef = moduleRef;
43
+ this.embeddingModels = embeddingModels;
41
44
  this.logger = new Logger('AiModule');
42
45
  }
46
+ /**
47
+ * Embeds one string, with the embedding model's settings, Nest policies and embedding usage
48
+ * events.
49
+ */
50
+ async embed(options) {
51
+ const plan = createEmbeddingPlan(options, this.embeddingModels, this.options, (name) => this.preset(name));
52
+ const { result, event, attempts, cost } = await runEmbedding(plan, { value: options.value });
53
+ this.report(plan, event, 'onEmbeddingUsage');
54
+ return {
55
+ value: result.value,
56
+ embedding: result.embedding,
57
+ usage: result.usage,
58
+ warnings: result.warnings,
59
+ ...(result.response !== undefined && { response: result.response }),
60
+ ...(result.providerMetadata !== undefined && { providerMetadata: result.providerMetadata }),
61
+ model: plan.model.info,
62
+ attempts,
63
+ ...(cost !== undefined && { cost }),
64
+ };
65
+ }
66
+ /**
67
+ * Embeds strings in input order; the SDK batches requests, with bounded concurrency (default 4).
68
+ * Timeouts, retries and policies apply to each request.
69
+ */
70
+ async embedMany(options) {
71
+ const plan = createEmbeddingPlan(options, this.embeddingModels, this.options, (name) => this.preset(name));
72
+ const { result, event, attempts, cost } = await runEmbedding(plan, { values: options.values });
73
+ this.report(plan, event, 'onEmbeddingUsage');
74
+ return {
75
+ values: result.values,
76
+ embeddings: result.embeddings,
77
+ usage: result.usage,
78
+ warnings: result.warnings,
79
+ ...(result.responses !== undefined && { responses: result.responses }),
80
+ ...(result.providerMetadata !== undefined && { providerMetadata: result.providerMetadata }),
81
+ model: plan.model.info,
82
+ attempts,
83
+ ...(cost !== undefined && { cost }),
84
+ };
85
+ }
43
86
  /** Generates text; with `tools`, calls them as the model asks, and answers when it is done. */
44
87
  async generateText(options) {
45
88
  const plan = this.plan(options, options?.abortSignal ?? NEVER_ABORTED);
@@ -116,24 +159,25 @@ let AiService = class AiService {
116
159
  return service.preset(name);
117
160
  }
118
161
  /** Calls the usage listeners. They never fail the call: what they throw is logged. */
119
- report(plan, event) {
162
+ report(plan, event, hook = 'onUsage') {
120
163
  for (const listener of plan.listeners) {
121
164
  try {
122
- Promise.resolve(listener(event)).catch((error) => this.logListenerError(error));
165
+ Promise.resolve(listener(event)).catch((error) => this.logListenerError(error, hook));
123
166
  }
124
167
  catch (error) {
125
- this.logListenerError(error);
168
+ this.logListenerError(error, hook);
126
169
  }
127
170
  }
128
171
  }
129
- logListenerError(error) {
130
- this.logger.error(`onUsage failed: ${error?.message ?? String(error)}`, error?.stack);
172
+ logListenerError(error, hook) {
173
+ this.logger.error(`${hook} failed: ${error?.message ?? String(error)}`, error?.stack);
131
174
  }
132
175
  };
133
176
  AiService = __decorate([
134
177
  Injectable(),
135
178
  __param(2, Inject(AI_MODULE_OPTIONS)),
136
179
  __metadata("design:paramtypes", [AiModels,
137
- AiTools, Object, ModuleRef])
180
+ AiTools, Object, ModuleRef,
181
+ AiEmbeddingModels])
138
182
  ], AiService);
139
183
  export { AiService };
@@ -0,0 +1,57 @@
1
+ import { MockEmbeddingModelV4 } from 'ai/test';
2
+ type CallOptions = Parameters<MockEmbeddingModelV4['doEmbed']>[0];
3
+ type EmbedResult = Awaited<ReturnType<MockEmbeddingModelV4['doEmbed']>>;
4
+ /**
5
+ * One provider response of a fake embedding model, including one vector for each value in that
6
+ * call.
7
+ */
8
+ export interface FakeEmbeddingTurn {
9
+ embeddings: number[][];
10
+ /** Default: 10 tokens per value. */
11
+ usage?: {
12
+ tokens: number;
13
+ };
14
+ /** Milliseconds before the answer; cancellation cuts the wait short. */
15
+ delay?: number;
16
+ warnings?: EmbedResult['warnings'];
17
+ providerMetadata?: EmbedResult['providerMetadata'];
18
+ response?: EmbedResult['response'];
19
+ }
20
+ /** A provider call the fake received. SDK batching can make several calls for one embedMany(). */
21
+ export interface FakeEmbeddingModelCall {
22
+ values: string[];
23
+ options: CallOptions;
24
+ }
25
+ /** An embedding response, provider error, or function of the provider call. */
26
+ export type FakeEmbeddingAnswer = FakeEmbeddingTurn | Error | ((call: FakeEmbeddingModelCall) => FakeEmbeddingAnswer | Promise<FakeEmbeddingAnswer>);
27
+ /** Provider capabilities used by the SDK when it batches input values. */
28
+ export interface FakeEmbeddingModelOptions {
29
+ provider?: string;
30
+ modelId?: string;
31
+ /** Omitted: no provider batch limit. */
32
+ maxEmbeddingsPerCall?: number;
33
+ /** Default true. */
34
+ supportsParallelCalls?: boolean;
35
+ }
36
+ /**
37
+ * An embedding model for offline tests. Answers provider calls in order, records them, and supports
38
+ * the SDK's batching. A function answers every call; reply() appends scripted responses. No vectors
39
+ * are generated implicitly.
40
+ *
41
+ * ```ts
42
+ * const model = new FakeEmbeddingModel([{ embeddings: [[0.1, 0.2]], usage: { tokens: 3 } }]);
43
+ * ```
44
+ */
45
+ export declare class FakeEmbeddingModel extends MockEmbeddingModelV4 {
46
+ readonly calls: FakeEmbeddingModelCall[];
47
+ private readonly answers;
48
+ private readonly answerAll;
49
+ constructor(answers?: FakeEmbeddingAnswer[] | ((call: FakeEmbeddingModelCall) => FakeEmbeddingAnswer | Promise<FakeEmbeddingAnswer>), { provider, modelId, maxEmbeddingsPerCall, supportsParallelCalls, }?: FakeEmbeddingModelOptions);
50
+ /** Adds responses after the unused scripted responses. */
51
+ reply(...answers: FakeEmbeddingAnswer[]): this;
52
+ /** The last provider call, or undefined. */
53
+ get lastCall(): FakeEmbeddingModelCall | undefined;
54
+ }
55
+ /** new FakeEmbeddingModel(answers). */
56
+ export declare function fakeEmbeddingModel(...answers: FakeEmbeddingAnswer[]): FakeEmbeddingModel;
57
+ export {};
@@ -0,0 +1,82 @@
1
+ import { MockEmbeddingModelV4 } from 'ai/test';
2
+ /**
3
+ * An embedding model for offline tests. Answers provider calls in order, records them, and supports
4
+ * the SDK's batching. A function answers every call; reply() appends scripted responses. No vectors
5
+ * are generated implicitly.
6
+ *
7
+ * ```ts
8
+ * const model = new FakeEmbeddingModel([{ embeddings: [[0.1, 0.2]], usage: { tokens: 3 } }]);
9
+ * ```
10
+ */
11
+ export class FakeEmbeddingModel extends MockEmbeddingModelV4 {
12
+ constructor(answers = [], { provider = 'fake', modelId = 'fake-embedding-model', maxEmbeddingsPerCall, supportsParallelCalls = true, } = {}) {
13
+ super({
14
+ provider,
15
+ modelId,
16
+ maxEmbeddingsPerCall: maxEmbeddingsPerCall ?? null,
17
+ supportsParallelCalls,
18
+ });
19
+ this.calls = [];
20
+ this.answers = Array.isArray(answers) ? [...answers] : [];
21
+ this.answerAll = typeof answers === 'function' ? answers : undefined;
22
+ this.doEmbed = async (options) => {
23
+ this.doEmbedCalls.push(options);
24
+ const call = { values: [...options.values], options };
25
+ this.calls.push(call);
26
+ options.abortSignal?.throwIfAborted();
27
+ let answer = this.answerAll ?? this.answers.shift();
28
+ if (answer === undefined) {
29
+ throw new Error(`FakeEmbeddingModel: no answer left for call ${this.calls.length}. Give it more answers.`);
30
+ }
31
+ while (typeof answer === 'function') {
32
+ answer = await answer(call);
33
+ }
34
+ if (answer instanceof Error) {
35
+ throw answer;
36
+ }
37
+ if (answer.delay) {
38
+ await sleep(answer.delay, options.abortSignal);
39
+ }
40
+ options.abortSignal?.throwIfAborted();
41
+ return {
42
+ embeddings: answer.embeddings,
43
+ usage: answer.usage ?? { tokens: call.values.length * 10 },
44
+ warnings: answer.warnings ?? [],
45
+ ...(answer.providerMetadata !== undefined && {
46
+ providerMetadata: answer.providerMetadata,
47
+ }),
48
+ ...(answer.response !== undefined && { response: answer.response }),
49
+ };
50
+ };
51
+ }
52
+ /** Adds responses after the unused scripted responses. */
53
+ reply(...answers) {
54
+ this.answers.push(...answers);
55
+ return this;
56
+ }
57
+ /** The last provider call, or undefined. */
58
+ get lastCall() {
59
+ return this.calls[this.calls.length - 1];
60
+ }
61
+ }
62
+ /** new FakeEmbeddingModel(answers). */
63
+ export function fakeEmbeddingModel(...answers) {
64
+ return new FakeEmbeddingModel(answers);
65
+ }
66
+ function sleep(ms, signal) {
67
+ return new Promise((resolve, reject) => {
68
+ if (signal?.aborted) {
69
+ reject(signal.reason);
70
+ return;
71
+ }
72
+ const timer = setTimeout(() => {
73
+ signal?.removeEventListener('abort', onAbort);
74
+ resolve();
75
+ }, ms);
76
+ const onAbort = () => {
77
+ clearTimeout(timer);
78
+ reject(signal.reason);
79
+ };
80
+ signal?.addEventListener('abort', onAbort, { once: true });
81
+ });
82
+ }
@@ -8,14 +8,20 @@
8
8
  */
9
9
  import type { INestApplicationContext } from '@nestjs/common';
10
10
  import type { TestingModuleBuilder } from '@nestjs/testing';
11
- import type { LanguageModel } from 'ai';
11
+ import type { EmbeddingModel, LanguageModel } from 'ai';
12
12
  import type { AiCaller } from '../interfaces/ai-tool.interface.js';
13
13
  export { FakeModel, fakeModel, providerError, type FakeAnswer, type FakeModelCall, type FakeToolCall, type FakeTurn } from './fake-model.js';
14
+ export { FakeEmbeddingModel, fakeEmbeddingModel, type FakeEmbeddingAnswer, type FakeEmbeddingModelCall, type FakeEmbeddingModelOptions, type FakeEmbeddingTurn, } from './fake-embedding-model.js';
14
15
  /**
15
16
  * Replaces configured models by name (`{ default: new FakeModel([...]) }`) in a testing module, keeping their other
16
17
  * options (pricing, fallback, timeouts). A name that is not configured fails at compile time.
17
18
  */
18
19
  export declare function overrideModels(builder: TestingModuleBuilder, models: Record<string, LanguageModel>): TestingModuleBuilder;
20
+ /**
21
+ * Replaces configured embedding models by name, preserving their settings and pricing. Unknown
22
+ * names fail at startup.
23
+ */
24
+ export declare function overrideEmbeddingModels(builder: TestingModuleBuilder, models: Record<string, EmbeddingModel>): TestingModuleBuilder;
19
25
  /**
20
26
  * Calls the agent function `name` with `input` as `caller`, as an agent's call does: the input validated by its
21
27
  * schema, its scopes checked, then its guards, interceptors, pipes and filters, then the method. Resolves with what the
@@ -1,6 +1,7 @@
1
- import { AI_MODEL_OVERRIDES } from '../ai.constants.js';
1
+ import { AI_EMBEDDING_MODEL_OVERRIDES, AI_MODEL_OVERRIDES } from '../ai.constants.js';
2
2
  import { AiTools } from '../tools/ai-tools.service.js';
3
3
  export { FakeModel, fakeModel, providerError } from './fake-model.js';
4
+ export { FakeEmbeddingModel, fakeEmbeddingModel, } from './fake-embedding-model.js';
4
5
  /**
5
6
  * Replaces configured models by name (`{ default: new FakeModel([...]) }`) in a testing module, keeping their other
6
7
  * options (pricing, fallback, timeouts). A name that is not configured fails at compile time.
@@ -8,6 +9,13 @@ export { FakeModel, fakeModel, providerError } from './fake-model.js';
8
9
  export function overrideModels(builder, models) {
9
10
  return builder.overrideProvider(AI_MODEL_OVERRIDES).useValue(models);
10
11
  }
12
+ /**
13
+ * Replaces configured embedding models by name, preserving their settings and pricing. Unknown
14
+ * names fail at startup.
15
+ */
16
+ export function overrideEmbeddingModels(builder, models) {
17
+ return builder.overrideProvider(AI_EMBEDDING_MODEL_OVERRIDES).useValue(models);
18
+ }
11
19
  /**
12
20
  * Calls the agent function `name` with `input` as `caller`, as an agent's call does: the input validated by its
13
21
  * schema, its scopes checked, then its guards, interceptors, pipes and filters, then the method. Resolves with what the
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
2
  "name": "@nestjs/ai",
3
- "version": "0.0.1",
4
- "description": "Nest - modern, fast, powerful node.js web framework (@ai)",
3
+ "version": "0.0.2",
4
+ "description": "Language models for NestJS: model calls with timeouts, retries and fallbacks, structured output, tools and agents, and streaming to the browser.",
5
+ "keywords": [
6
+ "nestjs",
7
+ "nest",
8
+ "ai",
9
+ "llm",
10
+ "agents",
11
+ "ai-agent",
12
+ "tools",
13
+ "tool-calling",
14
+ "openai",
15
+ "anthropic",
16
+ "ai-sdk",
17
+ "streaming",
18
+ "chat"
19
+ ],
5
20
  "author": "Kamil Mysliwiec",
6
21
  "license": "MIT",
7
22
  "url": "https://github.com/nestjs/ai#readme",