@nestjs/ai 0.0.0-stage → 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 (101) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +108 -2
  3. package/dist/ai.constants.d.ts +30 -0
  4. package/dist/ai.constants.js +34 -0
  5. package/dist/ai.module-definition.d.ts +23 -0
  6. package/dist/ai.module-definition.js +39 -0
  7. package/dist/ai.module.d.ts +11 -0
  8. package/dist/ai.module.js +45 -0
  9. package/dist/context/ai-execution-context.d.ts +32 -0
  10. package/dist/context/ai-execution-context.js +41 -0
  11. package/dist/context/ai-params.d.ts +20 -0
  12. package/dist/context/ai-params.js +44 -0
  13. package/dist/decorators/agent-function.decorator.d.ts +22 -0
  14. package/dist/decorators/agent-function.decorator.js +36 -0
  15. package/dist/decorators/index.d.ts +3 -0
  16. package/dist/decorators/index.js +3 -0
  17. package/dist/decorators/inject-embedding-model.decorator.d.ts +10 -0
  18. package/dist/decorators/inject-embedding-model.decorator.js +16 -0
  19. package/dist/decorators/inject-model.decorator.d.ts +10 -0
  20. package/dist/decorators/inject-model.decorator.js +16 -0
  21. package/dist/errors/ai.errors.d.ts +54 -0
  22. package/dist/errors/ai.errors.js +62 -0
  23. package/dist/errors/index.d.ts +1 -0
  24. package/dist/errors/index.js +1 -0
  25. package/dist/http/keep-alive.util.d.ts +6 -0
  26. package/dist/http/keep-alive.util.js +38 -0
  27. package/dist/http/node-response.util.d.ts +14 -0
  28. package/dist/http/node-response.util.js +104 -0
  29. package/dist/index.d.ts +14 -0
  30. package/dist/index.js +16 -0
  31. package/dist/interfaces/agent-function.interface.d.ts +53 -0
  32. package/dist/interfaces/agent-function.interface.js +1 -0
  33. package/dist/interfaces/ai-call-options.interface.d.ts +65 -0
  34. package/dist/interfaces/ai-call-options.interface.js +1 -0
  35. package/dist/interfaces/ai-embedding.interface.d.ts +79 -0
  36. package/dist/interfaces/ai-embedding.interface.js +1 -0
  37. package/dist/interfaces/ai-module-options.interface.d.ts +110 -0
  38. package/dist/interfaces/ai-module-options.interface.js +1 -0
  39. package/dist/interfaces/ai-result.interface.d.ts +57 -0
  40. package/dist/interfaces/ai-result.interface.js +1 -0
  41. package/dist/interfaces/ai-schema.interface.d.ts +44 -0
  42. package/dist/interfaces/ai-schema.interface.js +1 -0
  43. package/dist/interfaces/ai-tool.interface.d.ts +22 -0
  44. package/dist/interfaces/ai-tool.interface.js +1 -0
  45. package/dist/interfaces/ai-usage.interface.d.ts +30 -0
  46. package/dist/interfaces/ai-usage.interface.js +1 -0
  47. package/dist/interfaces/index.d.ts +8 -0
  48. package/dist/interfaces/index.js +8 -0
  49. package/dist/models/ai-embedding-models.service.d.ts +31 -0
  50. package/dist/models/ai-embedding-models.service.js +138 -0
  51. package/dist/models/ai-models.service.d.ts +29 -0
  52. package/dist/models/ai-models.service.js +112 -0
  53. package/dist/models/model-registry.util.d.ts +6 -0
  54. package/dist/models/model-registry.util.js +21 -0
  55. package/dist/models/model-requests.d.ts +5 -0
  56. package/dist/models/model-requests.js +20 -0
  57. package/dist/runtime/agent-loop.d.ts +30 -0
  58. package/dist/runtime/agent-loop.js +81 -0
  59. package/dist/runtime/call-plan.d.ts +54 -0
  60. package/dist/runtime/call-plan.js +105 -0
  61. package/dist/runtime/embedding-call.d.ts +38 -0
  62. package/dist/runtime/embedding-call.js +120 -0
  63. package/dist/runtime/model-policy.d.ts +53 -0
  64. package/dist/runtime/model-policy.js +202 -0
  65. package/dist/runtime/steps.d.ts +32 -0
  66. package/dist/runtime/steps.js +190 -0
  67. package/dist/runtime/text-stream.d.ts +64 -0
  68. package/dist/runtime/text-stream.js +211 -0
  69. package/dist/services/ai.service.d.ts +70 -0
  70. package/dist/services/ai.service.js +183 -0
  71. package/dist/testing/fake-embedding-model.d.ts +57 -0
  72. package/dist/testing/fake-embedding-model.js +82 -0
  73. package/dist/testing/fake-model.d.ts +96 -0
  74. package/dist/testing/fake-model.js +201 -0
  75. package/dist/testing/index.d.ts +32 -0
  76. package/dist/testing/index.js +27 -0
  77. package/dist/tools/ai-tools.service.d.ts +77 -0
  78. package/dist/tools/ai-tools.service.js +234 -0
  79. package/dist/tools/tool-output.util.d.ts +6 -0
  80. package/dist/tools/tool-output.util.js +38 -0
  81. package/dist/utils/duration.util.d.ts +3 -0
  82. package/dist/utils/duration.util.js +17 -0
  83. package/dist/utils/errors.util.d.ts +16 -0
  84. package/dist/utils/errors.util.js +35 -0
  85. package/dist/utils/names.util.d.ts +2 -0
  86. package/dist/utils/names.util.js +10 -0
  87. package/dist/utils/schema.util.d.ts +16 -0
  88. package/dist/utils/schema.util.js +70 -0
  89. package/dist/utils/usage.util.d.ts +10 -0
  90. package/dist/utils/usage.util.js +42 -0
  91. package/dist/workflows/ai-agent.interfaces.d.ts +70 -0
  92. package/dist/workflows/ai-agent.interfaces.js +1 -0
  93. package/dist/workflows/ai-agent.workflow.d.ts +15 -0
  94. package/dist/workflows/ai-agent.workflow.js +25 -0
  95. package/dist/workflows/ai-agents.service.d.ts +83 -0
  96. package/dist/workflows/ai-agents.service.js +277 -0
  97. package/dist/workflows/ai-workflows.module.d.ts +6 -0
  98. package/dist/workflows/ai-workflows.module.js +22 -0
  99. package/dist/workflows/index.d.ts +6 -0
  100. package/dist/workflows/index.js +5 -0
  101. package/package.json +115 -4
@@ -0,0 +1,183 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Inject, Injectable, Logger } from '@nestjs/common';
14
+ import { ModuleRef } from '@nestjs/core';
15
+ import { ResilienceService } from '@nestjs/resilience';
16
+ import { AI_MODULE_OPTIONS } from '../ai.constants.js';
17
+ import { AiConfigurationError, AiOutputError } from '../errors/ai.errors.js';
18
+ import { AiModels } from '../models/ai-models.service.js';
19
+ import { AiEmbeddingModels } from '../models/ai-embedding-models.service.js';
20
+ import { runLoop, usageEvent } from '../runtime/agent-loop.js';
21
+ import { createCallPlan } from '../runtime/call-plan.js';
22
+ import { createEmbeddingPlan, runEmbedding } from '../runtime/embedding-call.js';
23
+ import { runStep } from '../runtime/model-policy.js';
24
+ import { generateStep } from '../runtime/steps.js';
25
+ import { AiTextStream } from '../runtime/text-stream.js';
26
+ import { AiTools } from '../tools/ai-tools.service.js';
27
+ import { toModelSchema } from '../utils/schema.util.js';
28
+ const NEVER_ABORTED = new AbortController().signal;
29
+ /**
30
+ * Model calls: text, structured output and streams, with the application's agent functions as tools, through the
31
+ * configured models, with timeouts, retries, fallback, budgets and usage events.
32
+ *
33
+ * ```ts
34
+ * const { text } = await this.ai.generateText({ prompt: 'Summarize this ticket: ...' });
35
+ * ```
36
+ */
37
+ let AiService = class AiService {
38
+ constructor(models, tools, options, moduleRef, embeddingModels) {
39
+ this.models = models;
40
+ this.tools = tools;
41
+ this.options = options;
42
+ this.moduleRef = moduleRef;
43
+ this.embeddingModels = embeddingModels;
44
+ this.logger = new Logger('AiModule');
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
+ }
86
+ /** Generates text; with `tools`, calls them as the model asks, and answers when it is done. */
87
+ async generateText(options) {
88
+ const plan = this.plan(options, options?.abortSignal ?? NEVER_ABORTED);
89
+ const { result } = await runLoop(plan, { stream: false }, (event) => this.report(plan, event));
90
+ return result;
91
+ }
92
+ /**
93
+ * Generates an object that matches `schema`: an invoice from an email, a triage decision from a ticket. Rejects
94
+ * with `AiOutputError` when the answer does not match it.
95
+ */
96
+ async generateObject(options) {
97
+ const schema = toModelSchema(options?.schema, 'generateObject() `schema`');
98
+ const plan = this.plan(options, options.abortSignal ?? NEVER_ABORTED);
99
+ const { result, output } = await runLoop(plan, {
100
+ stream: false,
101
+ output: {
102
+ schema,
103
+ ...(options.schemaName !== undefined && { name: options.schemaName }),
104
+ ...(options.schemaDescription !== undefined && { description: options.schemaDescription }),
105
+ },
106
+ }, (event) => this.report(plan, event));
107
+ if (output === undefined) {
108
+ throw new AiOutputError(`The model was still calling tools after ${plan.maxSteps} step(s), and gave no answer. Raise \`maxSteps\`.`, result.text);
109
+ }
110
+ return { ...result, object: output };
111
+ }
112
+ /**
113
+ * Streams text: the call starts now, and its answer can be sent to the browser as it is generated
114
+ * (`pipeUIMessageStreamToResponse(res)`), read (`textStream`) or awaited (`text`). Mistakes in the options throw
115
+ * here; failures of the call reach the stream.
116
+ */
117
+ streamText(options) {
118
+ const controller = new AbortController();
119
+ const signal = options?.abortSignal ? AbortSignal.any([options.abortSignal, controller.signal]) : controller.signal;
120
+ const plan = this.plan(options, signal);
121
+ return new AiTextStream(controller, async (emit) => {
122
+ const { result, finish } = await runLoop(plan, { stream: true, emit }, (event) => this.report(plan, event));
123
+ emit(finish ?? { type: 'finish', finishReason: result.finishReason });
124
+ return result;
125
+ });
126
+ }
127
+ /**
128
+ * @internal One model call of a durable run (`@nestjs/ai/workflows`), with `messages` so far: the tools are offered
129
+ * and their calls returned, not run. Retries, timeouts, fallback and usage events as for any call.
130
+ */
131
+ async callModel(options, { index, messages, signal }) {
132
+ const plan = this.plan({ ...options, prompt: undefined, messages }, signal, false);
133
+ const { value, attempts } = await runStep(plan, async (attempt) => {
134
+ const outcome = await generateStep(plan, index, plan.messages, attempt);
135
+ this.report(plan, usageEvent(plan, attempt, outcome, false));
136
+ return outcome;
137
+ });
138
+ return { outcome: value, attempts };
139
+ }
140
+ plan(options, signal, toolExecution = true) {
141
+ return createCallPlan(options, {
142
+ models: this.models,
143
+ tools: this.tools,
144
+ moduleOptions: this.options,
145
+ signal,
146
+ preset: (name) => this.preset(name),
147
+ toolExecution,
148
+ });
149
+ }
150
+ /** A preset of the application's `ResilienceService`, which `ResilienceModule.forRoot({ presets })` configures. */
151
+ preset(name) {
152
+ let service;
153
+ try {
154
+ service = this.moduleRef.get(ResilienceService, { strict: false });
155
+ }
156
+ catch {
157
+ throw new AiConfigurationError(`\`resilience: '${name}'\` names a preset of ResilienceService, and the application does not import ResilienceModule.`);
158
+ }
159
+ return service.preset(name);
160
+ }
161
+ /** Calls the usage listeners. They never fail the call: what they throw is logged. */
162
+ report(plan, event, hook = 'onUsage') {
163
+ for (const listener of plan.listeners) {
164
+ try {
165
+ Promise.resolve(listener(event)).catch((error) => this.logListenerError(error, hook));
166
+ }
167
+ catch (error) {
168
+ this.logListenerError(error, hook);
169
+ }
170
+ }
171
+ }
172
+ logListenerError(error, hook) {
173
+ this.logger.error(`${hook} failed: ${error?.message ?? String(error)}`, error?.stack);
174
+ }
175
+ };
176
+ AiService = __decorate([
177
+ Injectable(),
178
+ __param(2, Inject(AI_MODULE_OPTIONS)),
179
+ __metadata("design:paramtypes", [AiModels,
180
+ AiTools, Object, ModuleRef,
181
+ AiEmbeddingModels])
182
+ ], AiService);
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
+ }
@@ -0,0 +1,96 @@
1
+ import { APICallError } from 'ai';
2
+ import { MockLanguageModelV4 } from 'ai/test';
3
+ import type { AiUsage } from '../interfaces/ai-usage.interface.js';
4
+ type CallOptions = Parameters<MockLanguageModelV4['doGenerate']>[0];
5
+ type GenerateResult = Awaited<ReturnType<MockLanguageModelV4['doGenerate']>>;
6
+ type FinishReason = GenerateResult['finishReason']['unified'];
7
+ /** A tool call the fake model makes. */
8
+ export interface FakeToolCall {
9
+ /** The tool's name. */
10
+ name: string;
11
+ /** Its arguments. Default `{}`. */
12
+ input?: unknown;
13
+ /** Default `call-<n>`. */
14
+ id?: string;
15
+ }
16
+ /** One answer of the fake model. */
17
+ export interface FakeTurn {
18
+ text?: string;
19
+ /** Answered as JSON text: what `generateObject()` reads. */
20
+ object?: unknown;
21
+ toolCalls?: FakeToolCall[];
22
+ /** Default: 10 input and 10 output tokens. */
23
+ usage?: Partial<Pick<AiUsage, 'inputTokens' | 'outputTokens' | 'cachedInputTokens' | 'reasoningTokens'>>;
24
+ /** Default `tool-calls` with tool calls, else `stop`. */
25
+ finishReason?: FinishReason;
26
+ /** Milliseconds before the model answers (a stream: before it opens). The call's abort cuts it short. */
27
+ delay?: number;
28
+ /** Milliseconds between a stream's chunks. */
29
+ chunkDelay?: number;
30
+ /** A stream fails with this error after its text, as a connection that breaks does. */
31
+ streamError?: unknown;
32
+ }
33
+ /**
34
+ * What the fake model answers a call with: text, a turn, an error (thrown, as a provider's failure: see
35
+ * `providerError()`), or a function of the call that returns one of these.
36
+ */
37
+ export type FakeAnswer = string | FakeTurn | Error | ((call: FakeModelCall) => FakeAnswer | Promise<FakeAnswer>);
38
+ /** A call the fake model received. */
39
+ export interface FakeModelCall {
40
+ stream: boolean;
41
+ /** The system prompt. */
42
+ instructions?: string;
43
+ /** The text of the last user message. */
44
+ lastUserMessage?: string;
45
+ /** The results of the tools called in the previous step, as the model reads them. */
46
+ toolResults: Array<{
47
+ toolName: string;
48
+ toolCallId: string;
49
+ output: unknown;
50
+ }>;
51
+ /** The names of the tools offered. */
52
+ tools: string[];
53
+ /** Everything the model was sent (`ai`'s provider-level call options). */
54
+ options: CallOptions;
55
+ }
56
+ /**
57
+ * A language model for tests, with no network: it answers each call with the next of its `answers` (text, tool
58
+ * calls, objects, errors, delays), and records the calls. Pass a function instead of a list to answer every call
59
+ * with it.
60
+ *
61
+ * ```ts
62
+ * const model = new FakeModel([
63
+ * { toolCalls: [{ name: 'find-order', input: { orderId: 'ord_1' } }] },
64
+ * 'Your order ships tomorrow.',
65
+ * ]);
66
+ * ```
67
+ */
68
+ export declare class FakeModel extends MockLanguageModelV4 {
69
+ readonly calls: FakeModelCall[];
70
+ private readonly answers;
71
+ private readonly answerAll;
72
+ private toolCallCount;
73
+ constructor(answers?: FakeAnswer[] | ((call: FakeModelCall) => FakeAnswer | Promise<FakeAnswer>), { provider, modelId }?: {
74
+ modelId?: string | undefined;
75
+ provider?: string | undefined;
76
+ });
77
+ /** Adds answers, after the ones not used yet. */
78
+ reply(...answers: FakeAnswer[]): this;
79
+ /** The last call, or `undefined`. */
80
+ get lastCall(): FakeModelCall | undefined;
81
+ private answer;
82
+ private generate;
83
+ private stream;
84
+ private toolCalls;
85
+ }
86
+ /** `new FakeModel(answers)`. */
87
+ export declare function fakeModel(...answers: FakeAnswer[]): FakeModel;
88
+ /**
89
+ * A provider's HTTP error, as `@ai-sdk/*` providers throw it: retryable for 408, 409, 429 and 5xx. `retryAfter` (in
90
+ * seconds) is sent as a `retry-after` header.
91
+ */
92
+ export declare function providerError(status: number, { message, retryAfter }?: {
93
+ message?: string;
94
+ retryAfter?: number;
95
+ }): APICallError;
96
+ export {};
@@ -0,0 +1,201 @@
1
+ import { APICallError } from 'ai';
2
+ import { MockLanguageModelV4 } from 'ai/test';
3
+ /**
4
+ * A language model for tests, with no network: it answers each call with the next of its `answers` (text, tool
5
+ * calls, objects, errors, delays), and records the calls. Pass a function instead of a list to answer every call
6
+ * with it.
7
+ *
8
+ * ```ts
9
+ * const model = new FakeModel([
10
+ * { toolCalls: [{ name: 'find-order', input: { orderId: 'ord_1' } }] },
11
+ * 'Your order ships tomorrow.',
12
+ * ]);
13
+ * ```
14
+ */
15
+ export class FakeModel extends MockLanguageModelV4 {
16
+ constructor(answers = [], { provider = 'fake', modelId = 'fake-model' } = {}) {
17
+ super({ provider, modelId });
18
+ this.calls = [];
19
+ this.toolCallCount = 0;
20
+ this.answers = Array.isArray(answers) ? [...answers] : [];
21
+ this.answerAll = typeof answers === 'function' ? answers : undefined;
22
+ this.doGenerate = async (options) => this.generate(await this.answer(options, false), options);
23
+ this.doStream = async (options) => this.stream(await this.answer(options, true), options);
24
+ }
25
+ /** Adds answers, after the ones not used yet. */
26
+ reply(...answers) {
27
+ this.answers.push(...answers);
28
+ return this;
29
+ }
30
+ /** The last call, or `undefined`. */
31
+ get lastCall() {
32
+ return this.calls[this.calls.length - 1];
33
+ }
34
+ async answer(options, stream) {
35
+ const call = describeCall(options, stream);
36
+ this.calls.push(call);
37
+ let answer = this.answerAll ?? this.answers.shift();
38
+ if (answer === undefined) {
39
+ throw new Error(`FakeModel: no answer left for call ${this.calls.length}. Give it more answers.`);
40
+ }
41
+ while (typeof answer === 'function') {
42
+ answer = await answer(call);
43
+ }
44
+ if (answer instanceof Error) {
45
+ throw answer;
46
+ }
47
+ const turn = typeof answer === 'string' ? { text: answer } : answer;
48
+ if (turn.delay) {
49
+ await sleep(turn.delay, options.abortSignal);
50
+ }
51
+ return turn;
52
+ }
53
+ generate(turn, options) {
54
+ options.abortSignal?.throwIfAborted();
55
+ const text = textOf(turn);
56
+ const toolCalls = this.toolCalls(turn);
57
+ return {
58
+ content: [...(text ? [{ type: 'text', text }] : []), ...toolCalls],
59
+ finishReason: finishOf(turn, toolCalls.length),
60
+ usage: usageOf(turn),
61
+ warnings: [],
62
+ response: { id: `response-${this.calls.length}`, modelId: this.modelId, timestamp: new Date(0) },
63
+ };
64
+ }
65
+ stream(turn, options) {
66
+ const text = textOf(turn);
67
+ const toolCalls = this.toolCalls(turn);
68
+ const parts = [
69
+ { type: 'stream-start', warnings: [] },
70
+ { type: 'response-metadata', id: `response-${this.calls.length}`, modelId: this.modelId, timestamp: new Date(0) },
71
+ ];
72
+ if (text) {
73
+ parts.push({ type: 'text-start', id: 'text-0' });
74
+ for (const delta of text.match(/\S+\s*|\s+/g) ?? []) {
75
+ parts.push({ type: 'text-delta', id: 'text-0', delta });
76
+ }
77
+ parts.push({ type: 'text-end', id: 'text-0' });
78
+ }
79
+ parts.push(...toolCalls);
80
+ if (turn.streamError !== undefined) {
81
+ parts.push({ type: 'error', error: turn.streamError });
82
+ }
83
+ else {
84
+ parts.push({ type: 'finish', finishReason: finishOf(turn, toolCalls.length), usage: usageOf(turn) });
85
+ }
86
+ const signal = options.abortSignal;
87
+ let index = 0;
88
+ return {
89
+ stream: new ReadableStream({
90
+ pull: async (controller) => {
91
+ if (index > 0 && turn.chunkDelay) {
92
+ try {
93
+ await sleep(turn.chunkDelay, signal);
94
+ }
95
+ catch (error) {
96
+ controller.error(error);
97
+ return;
98
+ }
99
+ }
100
+ if (signal?.aborted) {
101
+ controller.error(signal.reason);
102
+ return;
103
+ }
104
+ const part = parts[index++];
105
+ if (part === undefined) {
106
+ controller.close();
107
+ }
108
+ else {
109
+ controller.enqueue(part);
110
+ if (part.type === 'error') {
111
+ controller.close();
112
+ }
113
+ }
114
+ },
115
+ }),
116
+ };
117
+ }
118
+ toolCalls(turn) {
119
+ return (turn.toolCalls ?? []).map((call) => ({
120
+ type: 'tool-call',
121
+ toolCallId: call.id ?? `call-${++this.toolCallCount}`,
122
+ toolName: call.name,
123
+ input: JSON.stringify(call.input ?? {}),
124
+ }));
125
+ }
126
+ }
127
+ /** `new FakeModel(answers)`. */
128
+ export function fakeModel(...answers) {
129
+ return new FakeModel(answers);
130
+ }
131
+ /**
132
+ * A provider's HTTP error, as `@ai-sdk/*` providers throw it: retryable for 408, 409, 429 and 5xx. `retryAfter` (in
133
+ * seconds) is sent as a `retry-after` header.
134
+ */
135
+ export function providerError(status, { message, retryAfter } = {}) {
136
+ return new APICallError({
137
+ message: message ?? `The provider answered ${status}`,
138
+ url: 'https://fake-provider.test/v1/messages',
139
+ requestBodyValues: {},
140
+ statusCode: status,
141
+ ...(retryAfter !== undefined && { responseHeaders: { 'retry-after': String(retryAfter) } }),
142
+ });
143
+ }
144
+ function textOf(turn) {
145
+ return turn.object !== undefined ? JSON.stringify(turn.object) : (turn.text ?? '');
146
+ }
147
+ function finishOf(turn, toolCalls) {
148
+ const unified = turn.finishReason ?? (toolCalls > 0 ? 'tool-calls' : 'stop');
149
+ return { unified, raw: unified };
150
+ }
151
+ function usageOf(turn) {
152
+ const input = turn.usage?.inputTokens ?? 10;
153
+ const output = turn.usage?.outputTokens ?? 10;
154
+ const cached = turn.usage?.cachedInputTokens ?? 0;
155
+ const reasoning = turn.usage?.reasoningTokens ?? 0;
156
+ return {
157
+ inputTokens: { total: input, noCache: input - cached, cacheRead: cached, cacheWrite: undefined },
158
+ outputTokens: { total: output, text: output - reasoning, reasoning },
159
+ };
160
+ }
161
+ function describeCall(options, stream) {
162
+ const prompt = options.prompt;
163
+ const system = prompt.filter((message) => message.role === 'system').map((message) => message.content);
164
+ const lastUser = [...prompt].reverse().find((message) => message.role === 'user');
165
+ const trailingTools = [];
166
+ for (let index = prompt.length - 1; index >= 0 && prompt[index].role === 'tool'; index--) {
167
+ trailingTools.unshift(prompt[index]);
168
+ }
169
+ return {
170
+ stream,
171
+ ...(system.length > 0 && { instructions: system.join('\n') }),
172
+ ...(lastUser && {
173
+ lastUserMessage: lastUser.content
174
+ .filter((part) => part.type === 'text')
175
+ .map((part) => part.text)
176
+ .join(''),
177
+ }),
178
+ toolResults: trailingTools.flatMap((message) => message.content
179
+ .filter((part) => part.type === 'tool-result')
180
+ .map((part) => ({ toolName: part.toolName, toolCallId: part.toolCallId, output: part.output?.value }))),
181
+ tools: (options.tools ?? []).map((tool) => tool.name),
182
+ options,
183
+ };
184
+ }
185
+ function sleep(ms, signal) {
186
+ return new Promise((resolve, reject) => {
187
+ if (signal?.aborted) {
188
+ reject(signal.reason);
189
+ return;
190
+ }
191
+ const timer = setTimeout(() => {
192
+ signal?.removeEventListener('abort', onAbort);
193
+ resolve();
194
+ }, ms);
195
+ const onAbort = () => {
196
+ clearTimeout(timer);
197
+ reject(signal.reason);
198
+ };
199
+ signal?.addEventListener('abort', onAbort, { once: true });
200
+ });
201
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `@nestjs/ai/testing`: a fake model, and helpers for tests of applications that use `AiModule`.
3
+ *
4
+ * ```ts
5
+ * const model = new FakeModel(['Your order ships tomorrow.']);
6
+ * const moduleRef = await overrideModels(Test.createTestingModule({ imports: [AppModule] }), { default: model }).compile();
7
+ * ```
8
+ */
9
+ import type { INestApplicationContext } from '@nestjs/common';
10
+ import type { TestingModuleBuilder } from '@nestjs/testing';
11
+ import type { EmbeddingModel, LanguageModel } from 'ai';
12
+ import type { AiCaller } from '../interfaces/ai-tool.interface.js';
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';
15
+ /**
16
+ * Replaces configured models by name (`{ default: new FakeModel([...]) }`) in a testing module, keeping their other
17
+ * options (pricing, fallback, timeouts). A name that is not configured fails at compile time.
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;
25
+ /**
26
+ * Calls the agent function `name` with `input` as `caller`, as an agent's call does: the input validated by its
27
+ * schema, its scopes checked, then its guards, interceptors, pipes and filters, then the method. Resolves with what the
28
+ * model would read; rejects with the `AiToolError` it would read (a guard's `Forbidden resource`).
29
+ */
30
+ export declare function callTool(app: INestApplicationContext, name: string, input?: unknown, { caller }?: {
31
+ caller?: AiCaller;
32
+ }): Promise<unknown>;
@@ -0,0 +1,27 @@
1
+ import { AI_EMBEDDING_MODEL_OVERRIDES, AI_MODEL_OVERRIDES } from '../ai.constants.js';
2
+ import { AiTools } from '../tools/ai-tools.service.js';
3
+ export { FakeModel, fakeModel, providerError } from './fake-model.js';
4
+ export { FakeEmbeddingModel, fakeEmbeddingModel, } from './fake-embedding-model.js';
5
+ /**
6
+ * Replaces configured models by name (`{ default: new FakeModel([...]) }`) in a testing module, keeping their other
7
+ * options (pricing, fallback, timeouts). A name that is not configured fails at compile time.
8
+ */
9
+ export function overrideModels(builder, models) {
10
+ return builder.overrideProvider(AI_MODEL_OVERRIDES).useValue(models);
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
+ }
19
+ /**
20
+ * Calls the agent function `name` with `input` as `caller`, as an agent's call does: the input validated by its
21
+ * schema, its scopes checked, then its guards, interceptors, pipes and filters, then the method. Resolves with what the
22
+ * model would read; rejects with the `AiToolError` it would read (a guard's `Forbidden resource`).
23
+ */
24
+ export async function callTool(app, name, input = {}, { caller } = {}) {
25
+ await app.init();
26
+ return app.get(AiTools, { strict: false }).call(name, input, caller);
27
+ }