@outputai/llm 0.6.1-next.fc6a93e.0 → 0.7.0
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/package.json +20 -12
- package/src/ai_model.js +25 -146
- package/src/ai_model.spec.js +185 -762
- package/src/ai_provider.js +86 -0
- package/src/ai_provider.spec.js +147 -0
- package/src/index.d.ts +51 -16
- package/src/index.js +1 -4
- package/src/prompt/validations.js +4 -1
- package/src/prompt/validations.spec.js +65 -0
- package/src/utils/error_handler.js +25 -8
- package/src/utils/error_handler.spec.js +17 -2
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { FatalError, ValidationError, z } from '@outputai/core';
|
|
2
|
+
import { Agent, fetch } from 'undici';
|
|
3
|
+
// providers
|
|
4
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
5
|
+
import { createAzure } from '@ai-sdk/azure';
|
|
6
|
+
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
7
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
|
+
import { createPerplexity } from '@ai-sdk/perplexity';
|
|
9
|
+
import { createVertex } from '@ai-sdk/google-vertex';
|
|
10
|
+
|
|
11
|
+
/** This custom dispatcher has longer timeouts */
|
|
12
|
+
const customDispatcher = new Agent( {
|
|
13
|
+
headersTimeout: 15 * 60 * 1000, // 15 min
|
|
14
|
+
bodyTimeout: 15 * 60 * 1000
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
/** This custom fetch instance uses the custom dispatcher */
|
|
18
|
+
const customFetch = ( input, init ) => fetch( input, { dispatcher: customDispatcher, ...init } );
|
|
19
|
+
|
|
20
|
+
/** Available provider to initialize. */
|
|
21
|
+
const providerInitializers = {
|
|
22
|
+
anthropic: createAnthropic,
|
|
23
|
+
azure: createAzure,
|
|
24
|
+
bedrock: createAmazonBedrock,
|
|
25
|
+
openai: createOpenAI,
|
|
26
|
+
perplexity: createPerplexity,
|
|
27
|
+
vertex: createVertex
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Providers already initialized due usage */
|
|
31
|
+
const initializedProviders = {};
|
|
32
|
+
|
|
33
|
+
/** Providers registered by the user */
|
|
34
|
+
const registeredProviders = {};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get all available provider names, including shipped and registered.
|
|
38
|
+
* @returns {string[]} Provider names
|
|
39
|
+
*/
|
|
40
|
+
export const getProviderNames = () =>
|
|
41
|
+
new Set( Object.keys( providerInitializers ).concat( Object.keys( registeredProviders ) ) ).values().toArray();
|
|
42
|
+
|
|
43
|
+
const registerProviderSchema = z.object( {
|
|
44
|
+
name: z.string().min( 1, 'Provider name must be a non-empty string' ),
|
|
45
|
+
providerFn: z.function()
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Register or override an AI SDK provider factory by name.
|
|
50
|
+
* @param {string} name - Provider name used in prompt frontmatter
|
|
51
|
+
* @param {Function} providerFn - Factory function that receives a model id
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
export function registerProvider( name, providerFn ) {
|
|
55
|
+
const result = registerProviderSchema.safeParse( { name, providerFn } );
|
|
56
|
+
if ( !result.success ) {
|
|
57
|
+
throw new ValidationError( `Invalid provider registration: ${z.prettifyError( result.error )}` );
|
|
58
|
+
}
|
|
59
|
+
registeredProviders[name] = providerFn;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Return a provider by its name.
|
|
64
|
+
* Look for registered providers first.
|
|
65
|
+
* If none, looks for initialized providers.
|
|
66
|
+
* Finally, looks for available provider initializers, and if found, init it.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} name
|
|
69
|
+
* @returns {object} provider
|
|
70
|
+
*/
|
|
71
|
+
export const getProvider = name => {
|
|
72
|
+
const provider = registeredProviders[name] ?? initializedProviders[name];
|
|
73
|
+
|
|
74
|
+
if ( provider ) {
|
|
75
|
+
return provider;
|
|
76
|
+
}
|
|
77
|
+
if ( providerInitializers[name] ) {
|
|
78
|
+
try {
|
|
79
|
+
return initializedProviders[name] = providerInitializers[name]( { fetch: customFetch } );
|
|
80
|
+
} catch ( error ) {
|
|
81
|
+
throw new FatalError( `Failed to initialize provider "${name}": ${error.message}`, { cause: error } );
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new FatalError( `Unsupported provider "${name}"` );
|
|
86
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const SHIPPED_PROVIDERS = [
|
|
4
|
+
{ name: 'anthropic', pkg: '@ai-sdk/anthropic', exportName: 'createAnthropic' },
|
|
5
|
+
{ name: 'azure', pkg: '@ai-sdk/azure', exportName: 'createAzure' },
|
|
6
|
+
{ name: 'bedrock', pkg: '@ai-sdk/amazon-bedrock', exportName: 'createAmazonBedrock' },
|
|
7
|
+
{ name: 'openai', pkg: '@ai-sdk/openai', exportName: 'createOpenAI' },
|
|
8
|
+
{ name: 'perplexity', pkg: '@ai-sdk/perplexity', exportName: 'createPerplexity' },
|
|
9
|
+
{ name: 'vertex', pkg: '@ai-sdk/google-vertex', exportName: 'createVertex' }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const makeProviderModules = () => Object.fromEntries(
|
|
13
|
+
SHIPPED_PROVIDERS.map( ( { name, pkg, exportName } ) => [
|
|
14
|
+
pkg,
|
|
15
|
+
{
|
|
16
|
+
[exportName]: vi.fn( options => ( { name, options } ) )
|
|
17
|
+
}
|
|
18
|
+
] )
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const importWithMockedProviders = async ( modules = makeProviderModules() ) => {
|
|
22
|
+
await vi.resetModules();
|
|
23
|
+
|
|
24
|
+
for ( const { pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
25
|
+
vi.doMock( pkg, () => ( {
|
|
26
|
+
[exportName]: modules[pkg][exportName]
|
|
27
|
+
} ) );
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
modules,
|
|
32
|
+
...( await import( './ai_provider.js' ) )
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
afterEach( () => {
|
|
37
|
+
for ( const { pkg } of SHIPPED_PROVIDERS ) {
|
|
38
|
+
vi.doUnmock( pkg );
|
|
39
|
+
}
|
|
40
|
+
vi.resetModules();
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
describe( 'getProvider', () => {
|
|
45
|
+
it( 'does not initialize shipped providers on import', async () => {
|
|
46
|
+
const { modules } = await importWithMockedProviders();
|
|
47
|
+
|
|
48
|
+
for ( const { pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
49
|
+
expect( modules[pkg][exportName] ).not.toHaveBeenCalled();
|
|
50
|
+
}
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'initializes each shipped provider with custom fetch when requested', async () => {
|
|
54
|
+
const { modules, getProvider } = await importWithMockedProviders();
|
|
55
|
+
|
|
56
|
+
for ( const { name, pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
57
|
+
const provider = getProvider( name );
|
|
58
|
+
|
|
59
|
+
expect( provider ).toMatchObject( { name, options: { fetch: expect.any( Function ) } } );
|
|
60
|
+
expect( modules[pkg][exportName] ).toHaveBeenCalledWith( { fetch: expect.any( Function ) } );
|
|
61
|
+
}
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
it( 'can import and initialize all installed shipped providers', async () => {
|
|
65
|
+
const { getProvider } = await import( './ai_provider.js' );
|
|
66
|
+
|
|
67
|
+
for ( const { name } of SHIPPED_PROVIDERS ) {
|
|
68
|
+
expect( getProvider( name ) ).toEqual( expect.any( Function ) );
|
|
69
|
+
}
|
|
70
|
+
} );
|
|
71
|
+
|
|
72
|
+
it( 'caches initialized providers', async () => {
|
|
73
|
+
const { modules, getProvider } = await importWithMockedProviders();
|
|
74
|
+
|
|
75
|
+
const first = getProvider( 'openai' );
|
|
76
|
+
const second = getProvider( 'openai' );
|
|
77
|
+
|
|
78
|
+
expect( second ).toBe( first );
|
|
79
|
+
expect( modules['@ai-sdk/openai'].createOpenAI ).toHaveBeenCalledTimes( 1 );
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
it( 'uses registered providers before shipped providers', async () => {
|
|
83
|
+
const { modules, getProvider, registerProvider } = await importWithMockedProviders();
|
|
84
|
+
const customProvider = vi.fn( model => ( { provider: 'custom', model } ) );
|
|
85
|
+
|
|
86
|
+
registerProvider( 'openai', customProvider );
|
|
87
|
+
|
|
88
|
+
expect( getProvider( 'openai' ) ).toBe( customProvider );
|
|
89
|
+
expect( modules['@ai-sdk/openai'].createOpenAI ).not.toHaveBeenCalled();
|
|
90
|
+
} );
|
|
91
|
+
|
|
92
|
+
it( 'throws FatalError for unsupported providers', async () => {
|
|
93
|
+
const { getProvider } = await importWithMockedProviders();
|
|
94
|
+
|
|
95
|
+
expect( () => getProvider( 'not-real' ) ).toThrow( 'Unsupported provider "not-real"' );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'throws a friendly error when provider initialization fails', async () => {
|
|
99
|
+
const modules = makeProviderModules();
|
|
100
|
+
modules['@ai-sdk/openai'].createOpenAI.mockImplementation( () => {
|
|
101
|
+
throw new Error( 'Missing OpenAI API key' );
|
|
102
|
+
} );
|
|
103
|
+
const { getProvider } = await importWithMockedProviders( modules );
|
|
104
|
+
|
|
105
|
+
expect( () => getProvider( 'openai' ) ).toThrow(
|
|
106
|
+
'Failed to initialize provider "openai": Missing OpenAI API key'
|
|
107
|
+
);
|
|
108
|
+
} );
|
|
109
|
+
} );
|
|
110
|
+
|
|
111
|
+
describe( 'registerProvider', () => {
|
|
112
|
+
it( 'registers custom providers', async () => {
|
|
113
|
+
const { getProvider, getProviderNames, registerProvider } = await importWithMockedProviders();
|
|
114
|
+
const customProvider = vi.fn();
|
|
115
|
+
|
|
116
|
+
registerProvider( 'custom', customProvider );
|
|
117
|
+
|
|
118
|
+
expect( getProvider( 'custom' ) ).toBe( customProvider );
|
|
119
|
+
expect( getProviderNames() ).toContain( 'custom' );
|
|
120
|
+
} );
|
|
121
|
+
|
|
122
|
+
it( 'validates provider registration arguments', async () => {
|
|
123
|
+
const { registerProvider } = await importWithMockedProviders();
|
|
124
|
+
|
|
125
|
+
expect( () => registerProvider( '', vi.fn() ) ).toThrow( 'Provider name must be a non-empty string' );
|
|
126
|
+
expect( () => registerProvider( 'custom', 'not-a-function' ) ).toThrow( 'expected function, received string' );
|
|
127
|
+
} );
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
describe( 'getProviderNames', () => {
|
|
131
|
+
it( 'returns shipped and registered provider names without duplicates', async () => {
|
|
132
|
+
const { getProviderNames, registerProvider } = await importWithMockedProviders();
|
|
133
|
+
|
|
134
|
+
registerProvider( 'custom', vi.fn() );
|
|
135
|
+
registerProvider( 'openai', vi.fn() );
|
|
136
|
+
|
|
137
|
+
expect( getProviderNames() ).toEqual( [
|
|
138
|
+
'anthropic',
|
|
139
|
+
'azure',
|
|
140
|
+
'bedrock',
|
|
141
|
+
'openai',
|
|
142
|
+
'perplexity',
|
|
143
|
+
'vertex',
|
|
144
|
+
'custom'
|
|
145
|
+
] );
|
|
146
|
+
} );
|
|
147
|
+
} );
|
package/src/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
StreamTextResult as AIStreamTextResult,
|
|
7
7
|
ToolLoopAgent as AIToolLoopAgent,
|
|
8
8
|
ToolSet,
|
|
9
|
+
ModelMessage,
|
|
9
10
|
StreamTextOnFinishCallback,
|
|
10
11
|
generateText as aiGenerateText,
|
|
11
12
|
streamText as aiStreamText,
|
|
@@ -40,11 +41,6 @@ export type {
|
|
|
40
41
|
// Re-export the tool helper function, Output, smoothStream, stop condition helpers, and jsonSchema
|
|
41
42
|
export { tool, Output, smoothStream, stepCountIs, hasToolCall, jsonSchema } from 'ai';
|
|
42
43
|
|
|
43
|
-
// Web search tool factories
|
|
44
|
-
export { tavilySearch, tavilyExtract, tavilyCrawl, tavilyMap } from '@tavily/ai-sdk';
|
|
45
|
-
export { webSearch as exaSearch } from '@exalabs/ai-sdk';
|
|
46
|
-
export { perplexitySearch } from '@perplexity-ai/ai-sdk';
|
|
47
|
-
|
|
48
44
|
/**
|
|
49
45
|
* Represents a single message in a prompt conversation.
|
|
50
46
|
*
|
|
@@ -171,11 +167,40 @@ export type SkillsArg<Input = unknown> = Skill[] |
|
|
|
171
167
|
( ( input: Input ) => Skill[] | Promise<Skill[]> );
|
|
172
168
|
|
|
173
169
|
/** Prompt-owned AI SDK fields supplied by Output prompt files. */
|
|
174
|
-
type PromptOwnedTextOptions = 'model' | 'messages' | 'prompt';
|
|
170
|
+
type PromptOwnedTextOptions = 'model' | 'messages' | 'prompt' | 'tools';
|
|
175
171
|
type AnyAiOutput = AIOutputNamespace.Output<unknown, unknown, unknown>;
|
|
172
|
+
type CompatibleToolFunction = ( ...args: never[] ) => unknown | PromiseLike<unknown>;
|
|
173
|
+
type CompatibleApprovalFunction = ( ...args: never[] ) => boolean | PromiseLike<boolean>;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Structurally-compatible AI SDK tool shape.
|
|
177
|
+
*
|
|
178
|
+
* This intentionally avoids referencing AI SDK's concrete `Tool` schema types so tools
|
|
179
|
+
* from packages resolved with a different Zod peer instance remain assignable.
|
|
180
|
+
*/
|
|
181
|
+
export type CompatibleTool = {
|
|
182
|
+
description?: string;
|
|
183
|
+
title?: string;
|
|
184
|
+
providerOptions?: Record<string, unknown>;
|
|
185
|
+
inputSchema?: unknown;
|
|
186
|
+
parameters?: unknown;
|
|
187
|
+
execute?: CompatibleToolFunction;
|
|
188
|
+
onInputStart?: CompatibleToolFunction;
|
|
189
|
+
onInputDelta?: CompatibleToolFunction;
|
|
190
|
+
onInputAvailable?: CompatibleToolFunction;
|
|
191
|
+
needsApproval?: boolean | CompatibleApprovalFunction;
|
|
192
|
+
} & (
|
|
193
|
+
{ inputSchema: unknown } |
|
|
194
|
+
{ parameters: unknown } |
|
|
195
|
+
{ execute: CompatibleToolFunction }
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
/** AI SDK tools accepted by Output APIs without requiring one exact Zod peer instance. */
|
|
199
|
+
export type CompatibleToolSet = Record<string, CompatibleTool>;
|
|
176
200
|
|
|
177
201
|
/**
|
|
178
202
|
* AI SDK options accepted by generateText, with prompt-owned fields supplied by Output prompt files.
|
|
203
|
+
* `tools` is accepted separately as {@link CompatibleToolSet} to support third-party tool packages.
|
|
179
204
|
*
|
|
180
205
|
* @typeParam Tools - The tools available for the model to call
|
|
181
206
|
*/
|
|
@@ -186,6 +211,7 @@ export type GenerateTextAiSdkOptions<
|
|
|
186
211
|
|
|
187
212
|
/**
|
|
188
213
|
* AI SDK options accepted by streamText, with prompt-owned fields supplied by Output prompt files.
|
|
214
|
+
* `tools` is accepted separately as {@link CompatibleToolSet} to support third-party tool packages.
|
|
189
215
|
*
|
|
190
216
|
* @typeParam Tools - The tools available for the model to call
|
|
191
217
|
*/
|
|
@@ -205,7 +231,8 @@ type GenerateImagePromptWithImages = Exclude<GenerateImagePrompt, string>;
|
|
|
205
231
|
type GenerateImageInput = GenerateImagePromptWithImages['images'][number];
|
|
206
232
|
|
|
207
233
|
/** Agent {@link Agent.stream} options: same as AI SDK plus wrapped `onFinish` (adds `cost`). */
|
|
208
|
-
export type OutputAgentStreamParameters = Omit<AgentStreamParameters<never, ToolSet>, 'onFinish'> & {
|
|
234
|
+
export type OutputAgentStreamParameters = Omit<AgentStreamParameters<never, ToolSet>, 'onFinish' | 'tools'> & {
|
|
235
|
+
tools?: CompatibleToolSet;
|
|
209
236
|
onFinish?: WrappedStreamTextOnFinishCallback<ToolSet>;
|
|
210
237
|
};
|
|
211
238
|
|
|
@@ -224,7 +251,7 @@ export type OutputAgentConstructorParameters<
|
|
|
224
251
|
/** Static skill packages made available to the LLM */
|
|
225
252
|
skills?: Skill[];
|
|
226
253
|
/** AI SDK tools available during the reasoning loop */
|
|
227
|
-
tools?:
|
|
254
|
+
tools?: CompatibleToolSet;
|
|
228
255
|
/** Maximum tool-loop iterations when stopWhen is not specified (default: 10) */
|
|
229
256
|
maxSteps?: number;
|
|
230
257
|
/** Pluggable conversation store — opt-in, stateless by default */
|
|
@@ -232,7 +259,9 @@ export type OutputAgentConstructorParameters<
|
|
|
232
259
|
};
|
|
233
260
|
|
|
234
261
|
/** Agent generate options accepted by the underlying AI SDK agent. */
|
|
235
|
-
export type OutputAgentGenerateParameters = AgentCallParameters<never, ToolSet
|
|
262
|
+
export type OutputAgentGenerateParameters = Omit<AgentCallParameters<never, ToolSet>, 'tools'> & {
|
|
263
|
+
tools?: CompatibleToolSet;
|
|
264
|
+
};
|
|
236
265
|
|
|
237
266
|
/** Parameters accepted by {@link generateText}. */
|
|
238
267
|
export type GenerateTextParameters<
|
|
@@ -249,6 +278,8 @@ export type GenerateTextParameters<
|
|
|
249
278
|
skills?: SkillsArg<Record<string, string | number | boolean> | undefined>;
|
|
250
279
|
/** Used to create a default `stepCountIs(maxSteps)` when tools are present and `stopWhen` is omitted */
|
|
251
280
|
maxSteps?: number;
|
|
281
|
+
/** AI SDK tools, accepted structurally to tolerate different Zod peer versions. */
|
|
282
|
+
tools?: CompatibleToolSet;
|
|
252
283
|
} & GenerateTextAiSdkOptions<Tools, OutputSpec>;
|
|
253
284
|
|
|
254
285
|
/** Parameters accepted by {@link streamText}. */
|
|
@@ -266,6 +297,8 @@ export type StreamTextParameters<
|
|
|
266
297
|
skills?: Skill[] | ( ( input: Record<string, string | number | boolean> | undefined ) => Skill[] );
|
|
267
298
|
/** Used to create a default `stepCountIs(maxSteps)` when tools are present and `stopWhen` is omitted */
|
|
268
299
|
maxSteps?: number;
|
|
300
|
+
/** AI SDK tools, accepted structurally to tolerate different Zod peer versions. */
|
|
301
|
+
tools?: CompatibleToolSet;
|
|
269
302
|
/** Callback when stream finishes. Receives the wrapped event with optional `cost`. */
|
|
270
303
|
onFinish?: WrappedStreamTextOnFinishCallback<Tools>;
|
|
271
304
|
} & Omit<StreamTextAiSdkOptions<Tools, OutputSpec>, 'onFinish'>;
|
|
@@ -391,15 +424,16 @@ export function registerProvider(
|
|
|
391
424
|
*
|
|
392
425
|
* @returns Array of provider name strings
|
|
393
426
|
*/
|
|
394
|
-
export function
|
|
427
|
+
export function getProviderNames(): string[];
|
|
395
428
|
|
|
396
429
|
/**
|
|
397
430
|
* Use an LLM model to generate text.
|
|
398
431
|
*
|
|
399
432
|
* This function is a wrapper over the AI SDK's `generateText`.
|
|
400
433
|
* The prompt file sets `model`, `messages`, `temperature`, `maxTokens`, and `providerOptions`.
|
|
401
|
-
*
|
|
402
|
-
*
|
|
434
|
+
* AI SDK-compatible `tools` are accepted structurally via {@link CompatibleToolSet}. Other AI SDK
|
|
435
|
+
* `generateText` options are accepted via {@link GenerateTextAiSdkOptions}, including tool choice,
|
|
436
|
+
* structured output, callbacks, retries, and sampling settings.
|
|
403
437
|
*
|
|
404
438
|
* @param args - Generation arguments. See {@link GenerateTextParameters}.
|
|
405
439
|
* @returns AI SDK response with text and metadata.
|
|
@@ -416,8 +450,9 @@ export function generateText<
|
|
|
416
450
|
*
|
|
417
451
|
* This function is a wrapper over the AI SDK's `streamText`.
|
|
418
452
|
* The prompt file sets `model`, `messages`, `temperature`, `maxTokens`, and `providerOptions`.
|
|
419
|
-
*
|
|
420
|
-
* `
|
|
453
|
+
* AI SDK-compatible `tools` are accepted structurally via {@link CompatibleToolSet}. Other AI SDK
|
|
454
|
+
* `streamText` options are accepted via {@link StreamTextAiSdkOptions}, except `onFinish`, which
|
|
455
|
+
* Output wraps to add optional cost data.
|
|
421
456
|
*
|
|
422
457
|
* @param args - Streaming arguments. See {@link StreamTextParameters}.
|
|
423
458
|
* @returns AI SDK stream result with textStream, fullStream, and metadata promises.
|
|
@@ -463,8 +498,8 @@ export function skill( params: {
|
|
|
463
498
|
|
|
464
499
|
/** Pluggable conversation store for multi-turn Agent interactions. */
|
|
465
500
|
export interface ConversationStore {
|
|
466
|
-
getMessages():
|
|
467
|
-
addMessages( messages:
|
|
501
|
+
getMessages(): ModelMessage[] | Promise<ModelMessage[]>;
|
|
502
|
+
addMessages( messages: ModelMessage[] ): void | Promise<void>;
|
|
468
503
|
}
|
|
469
504
|
|
|
470
505
|
/** Create an in-memory conversation store backed by a closure array. */
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
export { generateText, streamText, generateImage } from './ai_sdk.js';
|
|
2
2
|
export { Agent, createMemoryConversationStore, skill } from './agent.js';
|
|
3
3
|
export { loadPrompt } from './prompt/loader.js';
|
|
4
|
-
export { registerProvider,
|
|
5
|
-
export { tavilySearch, tavilyExtract, tavilyCrawl, tavilyMap } from '@tavily/ai-sdk';
|
|
6
|
-
export { webSearch as exaSearch } from '@exalabs/ai-sdk';
|
|
7
|
-
export { perplexitySearch } from '@perplexity-ai/ai-sdk';
|
|
4
|
+
export { registerProvider, getProviderNames } from './ai_provider.js';
|
|
8
5
|
export { tool, Output, smoothStream, stepCountIs, hasToolCall, jsonSchema } from 'ai';
|
|
9
6
|
export * as ai from 'ai';
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { ValidationError, z } from '@outputai/core';
|
|
2
2
|
|
|
3
|
+
const toolConfigSchema = z.record( z.string(), z.unknown() );
|
|
4
|
+
const toolsConfigSchema = z.record( z.string(), toolConfigSchema );
|
|
5
|
+
|
|
3
6
|
export const promptSchema = z.object( {
|
|
4
7
|
name: z.string(),
|
|
5
8
|
config: z.object( {
|
|
@@ -13,7 +16,7 @@ export const promptSchema = z.object( {
|
|
|
13
16
|
aspectRatio: z.string().regex( /^\d+:\d+$/ ).optional(),
|
|
14
17
|
seed: z.number().int().optional(),
|
|
15
18
|
skills: z.union( [ z.string().min( 1 ), z.array( z.string().min( 1 ) ) ] ).optional(),
|
|
16
|
-
tools:
|
|
19
|
+
tools: toolsConfigSchema.optional(),
|
|
17
20
|
providerOptions: z.object( {
|
|
18
21
|
thinking: z.object( {
|
|
19
22
|
type: z.enum( [ 'enabled', 'disabled' ] ),
|
|
@@ -384,6 +384,71 @@ describe( 'validatePrompt', () => {
|
|
|
384
384
|
expect( () => validatePrompt( promptWithSkill ) ).not.toThrow();
|
|
385
385
|
} );
|
|
386
386
|
|
|
387
|
+
it( 'should validate provider tool config records', () => {
|
|
388
|
+
const promptWithTools = {
|
|
389
|
+
name: 'tools-prompt',
|
|
390
|
+
config: {
|
|
391
|
+
provider: 'vertex',
|
|
392
|
+
model: 'gemini-2.0-flash',
|
|
393
|
+
tools: {
|
|
394
|
+
googleSearch: {
|
|
395
|
+
mode: 'MODE_DYNAMIC',
|
|
396
|
+
dynamicThreshold: 0.8
|
|
397
|
+
},
|
|
398
|
+
urlContext: {}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
messages: [
|
|
402
|
+
{
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: 'Research this.'
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
expect( () => validatePrompt( promptWithTools ) ).not.toThrow();
|
|
410
|
+
} );
|
|
411
|
+
|
|
412
|
+
it( 'should throw ValidationError when tools config is not a record', () => {
|
|
413
|
+
const invalidToolsPrompt = {
|
|
414
|
+
name: 'invalid-tools-prompt',
|
|
415
|
+
config: {
|
|
416
|
+
provider: 'vertex',
|
|
417
|
+
model: 'gemini-2.0-flash',
|
|
418
|
+
tools: [ 'googleSearch' ]
|
|
419
|
+
},
|
|
420
|
+
messages: [
|
|
421
|
+
{
|
|
422
|
+
role: 'user',
|
|
423
|
+
content: 'Research this.'
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
expect( () => validatePrompt( invalidToolsPrompt ) ).toThrow( ValidationError );
|
|
429
|
+
} );
|
|
430
|
+
|
|
431
|
+
it( 'should throw ValidationError when a tool config is not a record', () => {
|
|
432
|
+
const invalidToolConfigPrompt = {
|
|
433
|
+
name: 'invalid-tool-config-prompt',
|
|
434
|
+
config: {
|
|
435
|
+
provider: 'vertex',
|
|
436
|
+
model: 'gemini-2.0-flash',
|
|
437
|
+
tools: {
|
|
438
|
+
googleSearch: 'MODE_DYNAMIC'
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
messages: [
|
|
442
|
+
{
|
|
443
|
+
role: 'user',
|
|
444
|
+
content: 'Research this.'
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
expect( () => validatePrompt( invalidToolConfigPrompt ) ).toThrow( ValidationError );
|
|
450
|
+
} );
|
|
451
|
+
|
|
387
452
|
it( 'should throw ValidationError for invalid skill path config', () => {
|
|
388
453
|
const invalidSkillsPrompt = {
|
|
389
454
|
name: 'invalid-skills-prompt',
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
} from 'ai';
|
|
14
14
|
import { FatalError } from '@outputai/core';
|
|
15
15
|
|
|
16
|
+
// AI SDK does not expose a dedicated schema-mismatch discriminator for NoObjectGeneratedError.
|
|
17
|
+
const NO_OBJECT_SCHEMA_MISMATCH_MESSAGE = 'No object generated: response did not match schema.';
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Recursively search an error cause chain until finds an error which is instance of given prototype.
|
|
18
21
|
*
|
|
@@ -25,7 +28,7 @@ export const findInstanceInCauseChain = ( error, _class, depth = 0 ) => {
|
|
|
25
28
|
if ( !error || typeof error !== 'object' ) {
|
|
26
29
|
return null;
|
|
27
30
|
}
|
|
28
|
-
if ( typeof _class === 'string' && error.constructor
|
|
31
|
+
if ( typeof _class === 'string' && error.constructor?.name === _class ) {
|
|
29
32
|
return error;
|
|
30
33
|
}
|
|
31
34
|
if ( typeof _class === 'function' && error instanceof _class ) {
|
|
@@ -42,20 +45,34 @@ const toFatalError = ( error, extraMessage = '' ) => new FatalError(
|
|
|
42
45
|
{ cause: error }
|
|
43
46
|
);
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Map an AI SDK error to a framework specific error:
|
|
50
|
+
*
|
|
51
|
+
* - AI SDK Unrecoverable errors become FatalErrors, check code to see options.
|
|
52
|
+
* - NoObjectGeneratedError from invalid schema are reinitialized with a better message.
|
|
53
|
+
* - Other errors are preserved.
|
|
54
|
+
* @param {object} error - Original Error
|
|
55
|
+
* @returns {object} A new Error
|
|
56
|
+
*/
|
|
45
57
|
export const mapAiError = error => {
|
|
46
58
|
if ( error instanceof FatalError ) {
|
|
47
59
|
return error;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
// NoObjectGeneratedError can be thrown when the response doesn't match the schema
|
|
51
|
-
// This
|
|
52
|
-
if ( NoObjectGeneratedError.isInstance( error ) && error.message.includes(
|
|
62
|
+
// NoObjectGeneratedError can be thrown when the response doesn't match the schema.
|
|
63
|
+
// This re-creates the error with a better message, making it easier to debug.
|
|
64
|
+
if ( NoObjectGeneratedError.isInstance( error ) && error.message.includes( NO_OBJECT_SCHEMA_MISMATCH_MESSAGE ) ) {
|
|
53
65
|
const zodError = findInstanceInCauseChain( error, 'ZodError' );
|
|
54
66
|
if ( zodError && zodError.issues?.length > 0 ) {
|
|
55
|
-
const { path, message } = zodError.issues
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const [ { path, message } ] = zodError.issues;
|
|
68
|
+
return new NoObjectGeneratedError( {
|
|
69
|
+
message: `${error.message} First issue is "${message}" at path [${path.join( ', ' )}].`,
|
|
70
|
+
cause: error.cause,
|
|
71
|
+
text: error.text,
|
|
72
|
+
response: error.response,
|
|
73
|
+
usage: error.usage,
|
|
74
|
+
finishReason: error.finishReason
|
|
75
|
+
} );
|
|
59
76
|
}
|
|
60
77
|
return error;
|
|
61
78
|
}
|
|
@@ -171,6 +171,13 @@ describe( 'findInstanceInCauseChain', () => {
|
|
|
171
171
|
expect( findInstanceInCauseChain( 'not an error', Error ) ).toBeNull();
|
|
172
172
|
} );
|
|
173
173
|
|
|
174
|
+
it( 'returns null for object causes without constructors', () => {
|
|
175
|
+
const cause = Object.create( null );
|
|
176
|
+
const error = new FirstCustomError( 'first', { cause } );
|
|
177
|
+
|
|
178
|
+
expect( findInstanceInCauseChain( error, 'SecondCustomError' ) ).toBeNull();
|
|
179
|
+
} );
|
|
180
|
+
|
|
174
181
|
it( 'stops searching after the depth limit', () => {
|
|
175
182
|
const makeErrorChain = depth => depth === 0 ?
|
|
176
183
|
new SecondCustomError( 'target' ) :
|
|
@@ -204,17 +211,25 @@ describe( 'mapAiError', () => {
|
|
|
204
211
|
const error = new NoObjectGeneratedError( {
|
|
205
212
|
message: 'No object generated: response did not match schema.',
|
|
206
213
|
text: '{"items":[{}]}',
|
|
214
|
+
response: { id: 'response-1' },
|
|
215
|
+
usage: { totalTokens: 10 },
|
|
216
|
+
finishReason: 'stop',
|
|
207
217
|
cause: validationError
|
|
208
218
|
} );
|
|
209
219
|
|
|
210
220
|
const result = mapAiError( error );
|
|
211
221
|
|
|
212
222
|
expect( result ).not.toBe( error );
|
|
213
|
-
expect(
|
|
223
|
+
expect( NoObjectGeneratedError.isInstance( result ) ).toBe( true );
|
|
224
|
+
expect( result.name ).toBe( 'AI_NoObjectGeneratedError' );
|
|
214
225
|
expect( result.message ).toBe(
|
|
215
226
|
'No object generated: response did not match schema. First issue is "Expected string" at path [items, 0, title].'
|
|
216
227
|
);
|
|
217
|
-
expect( result.cause ).toBe(
|
|
228
|
+
expect( result.cause ).toBe( validationError );
|
|
229
|
+
expect( result.text ).toBe( error.text );
|
|
230
|
+
expect( result.response ).toBe( error.response );
|
|
231
|
+
expect( result.usage ).toBe( error.usage );
|
|
232
|
+
expect( result.finishReason ).toBe( error.finishReason );
|
|
218
233
|
} );
|
|
219
234
|
|
|
220
235
|
it( 'preserves NoObjectGeneratedError schema mismatches when no schema issue is available', () => {
|