@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.
- package/README.md +1 -1
- package/dist/ai.constants.d.ts +8 -0
- package/dist/ai.constants.js +10 -0
- package/dist/ai.module-definition.d.ts +2 -2
- package/dist/ai.module-definition.js +14 -5
- package/dist/ai.module.js +13 -4
- package/dist/decorators/index.d.ts +1 -0
- package/dist/decorators/index.js +1 -0
- package/dist/decorators/inject-embedding-model.decorator.d.ts +10 -0
- package/dist/decorators/inject-embedding-model.decorator.js +16 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/interfaces/ai-embedding.interface.d.ts +79 -0
- package/dist/interfaces/ai-embedding.interface.js +1 -0
- package/dist/interfaces/ai-module-options.interface.d.ts +31 -9
- package/dist/interfaces/ai-result.interface.d.ts +4 -1
- package/dist/interfaces/index.d.ts +1 -0
- package/dist/interfaces/index.js +1 -0
- package/dist/models/ai-embedding-models.service.d.ts +31 -0
- package/dist/models/ai-embedding-models.service.js +138 -0
- package/dist/models/ai-models.service.js +3 -4
- package/dist/models/model-registry.util.d.ts +6 -0
- package/dist/models/model-registry.util.js +21 -0
- package/dist/models/model-requests.d.ts +3 -0
- package/dist/models/model-requests.js +8 -0
- package/dist/runtime/embedding-call.d.ts +38 -0
- package/dist/runtime/embedding-call.js +120 -0
- package/dist/runtime/model-policy.d.ts +14 -9
- package/dist/runtime/model-policy.js +7 -7
- package/dist/services/ai.service.d.ts +14 -1
- package/dist/services/ai.service.js +51 -7
- package/dist/testing/fake-embedding-model.d.ts +57 -0
- package/dist/testing/fake-embedding-model.js +82 -0
- package/dist/testing/index.d.ts +7 -1
- package/dist/testing/index.js +9 -1
- 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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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
|
package/dist/testing/index.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "
|
|
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",
|