@outputai/core 0.15.1-next.15a30ef.0 → 0.15.1-next.2156c4b.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 +49 -5
- package/src/helpers/path.js +36 -0
- package/src/llm/agent.js +141 -0
- package/src/llm/ai_provider.js +131 -0
- package/src/llm/ai_sdk_options.js +126 -0
- package/src/llm/consts.js +6 -0
- package/src/llm/deprecated_provider_aliases.js +8 -0
- package/src/llm/generate.js +88 -0
- package/src/llm/index.d.ts +631 -0
- package/src/llm/index.js +5 -0
- package/src/llm/prompt/content.js +70 -0
- package/src/llm/prompt/interpolations.js +58 -0
- package/src/llm/prompt/loader.js +67 -0
- package/src/llm/prompt/markup/attributes.js +52 -0
- package/src/llm/prompt/markup/nodes.js +71 -0
- package/src/llm/prompt/markup/tokenizer.js +3 -0
- package/src/llm/prompt/markup/tokens.js +26 -0
- package/src/llm/prompt/validations.js +129 -0
- package/src/llm/utils/cost.js +150 -0
- package/src/llm/utils/error_handler.js +53 -0
- package/src/llm/utils/file.js +42 -0
- package/src/llm/utils/google_vertex_auth.js +31 -0
- package/src/llm/utils/grounding.js +49 -0
- package/src/llm/utils/image.js +37 -0
- package/src/llm/utils/legacy_cost_attribute.js +75 -0
- package/src/llm/utils/metering.js +126 -0
- package/src/llm/utils/models.js +15 -0
- package/src/llm/utils/models_pricing.js +93 -0
- package/src/llm/utils/models_pricing_snapshot.json +4166 -0
- package/src/llm/utils/skills.js +74 -0
- package/src/llm/utils/sources.js +73 -0
- package/src/llm/utils/stream.js +75 -0
- package/src/llm/utils/tools.js +48 -0
- package/src/llm/utils/usage.js +156 -0
- package/src/llm/utils/usage_tools.js +23 -0
- package/src/llm/utils/wrap.js +277 -0
- package/src/llm/validations.js +234 -0
- package/src/sdk/helpers/path.d.ts +2 -0
- package/src/sdk/helpers/path.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/core",
|
|
3
|
-
"version": "0.15.1-next.
|
|
3
|
+
"version": "0.15.1-next.2156c4b.0",
|
|
4
4
|
"description": "The core module of the output framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
"./http": {
|
|
35
35
|
"types": "./src/http/index.d.ts",
|
|
36
36
|
"import": "./src/http/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./llm": {
|
|
39
|
+
"types": "./src/llm/index.d.ts",
|
|
40
|
+
"import": "./src/llm/index.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"files": [
|
|
@@ -41,7 +45,8 @@
|
|
|
41
45
|
"./dist",
|
|
42
46
|
"./bin",
|
|
43
47
|
"!**/*.spec.*",
|
|
44
|
-
"!src/credentials"
|
|
48
|
+
"!src/credentials",
|
|
49
|
+
"!src/llm/fixtures"
|
|
45
50
|
],
|
|
46
51
|
"bin": {
|
|
47
52
|
"output-worker": "./bin/worker.sh",
|
|
@@ -62,9 +67,13 @@
|
|
|
62
67
|
"@temporalio/common": "1.24.0",
|
|
63
68
|
"@temporalio/worker": "1.24.0",
|
|
64
69
|
"@temporalio/workflow": "1.24.0",
|
|
70
|
+
"decimal.js": "10.6.0",
|
|
71
|
+
"entities": "8.1.0",
|
|
65
72
|
"folder-hash": "4.1.3",
|
|
73
|
+
"gray-matter": "4.0.3",
|
|
66
74
|
"js-yaml": "5.4.2",
|
|
67
75
|
"json-stream-stringify": "3.1.7",
|
|
76
|
+
"liquidjs": "10.29.0",
|
|
68
77
|
"redis": "5.12.1",
|
|
69
78
|
"stacktrace-parser": "0.1.11",
|
|
70
79
|
"winston": "3.19.0"
|
|
@@ -72,13 +81,47 @@
|
|
|
72
81
|
"peerDependencies": {
|
|
73
82
|
"zod": ">=4.3 <5",
|
|
74
83
|
"ky": ">=2 <3",
|
|
75
|
-
"undici": ">=8 <9"
|
|
84
|
+
"undici": ">=8 <9",
|
|
85
|
+
"ai": ">=7 <8",
|
|
86
|
+
"@ai-sdk/amazon-bedrock": ">=5 <6",
|
|
87
|
+
"@ai-sdk/anthropic": ">=4 <5",
|
|
88
|
+
"@ai-sdk/azure": ">=4 <5",
|
|
89
|
+
"@ai-sdk/google-vertex": ">=5 <6",
|
|
90
|
+
"@ai-sdk/openai": ">=4 <5",
|
|
91
|
+
"@ai-sdk/perplexity": ">=4 <5"
|
|
92
|
+
},
|
|
93
|
+
"peerDependenciesMeta": {
|
|
94
|
+
"@ai-sdk/amazon-bedrock": {
|
|
95
|
+
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@ai-sdk/anthropic": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@ai-sdk/azure": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"@ai-sdk/google-vertex": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@ai-sdk/openai": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@ai-sdk/perplexity": {
|
|
110
|
+
"optional": true
|
|
111
|
+
}
|
|
76
112
|
},
|
|
77
113
|
"devDependencies": {
|
|
78
114
|
"@types/js-yaml": "4.0.9",
|
|
79
115
|
"zod": "4.6.5",
|
|
80
116
|
"ky": "2.0.2",
|
|
81
|
-
"undici": "8.
|
|
117
|
+
"undici": "8.10.2",
|
|
118
|
+
"ai": "7.0.112",
|
|
119
|
+
"@ai-sdk/amazon-bedrock": "5.0.92",
|
|
120
|
+
"@ai-sdk/anthropic": "4.0.61",
|
|
121
|
+
"@ai-sdk/azure": "4.0.77",
|
|
122
|
+
"@ai-sdk/google-vertex": "5.0.91",
|
|
123
|
+
"@ai-sdk/openai": "4.0.73",
|
|
124
|
+
"@ai-sdk/perplexity": "4.0.49"
|
|
82
125
|
},
|
|
83
126
|
"license": "Apache-2.0",
|
|
84
127
|
"publishConfig": {
|
|
@@ -101,6 +144,7 @@
|
|
|
101
144
|
},
|
|
102
145
|
"scripts": {
|
|
103
146
|
"worker": "node ./src/worker/index.js",
|
|
104
|
-
"build": "rm -rf ./dist && tsc && pnpm exec tsc -p ./tsconfig.typecheck.json"
|
|
147
|
+
"build": "rm -rf ./dist && tsc && pnpm exec tsc -p ./tsconfig.typecheck.json",
|
|
148
|
+
"take-models-pricing-snapshot": "node ./scripts/take_models_pricing_snapshot.js"
|
|
105
149
|
}
|
|
106
150
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as stackTraceParser from 'stacktrace-parser';
|
|
2
|
+
|
|
3
|
+
// OS separator, but in a deterministic way, allowing this to work in Temporal's sandbox
|
|
4
|
+
// This avoids importing from node:path
|
|
5
|
+
const SEP = new Error().stack.includes( '/' ) ? '/' : '\\';
|
|
6
|
+
const transformSeparators = path => path.replaceAll( '/', SEP );
|
|
7
|
+
|
|
8
|
+
const defaultIgnorePaths = [
|
|
9
|
+
'/@outputai/core/',
|
|
10
|
+
'/@outputai/evals/',
|
|
11
|
+
'/sdk/core/',
|
|
12
|
+
'/sdk/evals/',
|
|
13
|
+
'node:internal/',
|
|
14
|
+
'evalmachine.',
|
|
15
|
+
'webpack/bootstrap'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the first immediate directory of the file invoking the code that called this function.
|
|
20
|
+
*
|
|
21
|
+
* Excludes `@outputai/core`, node, other internal paths, and any additional ignore paths.
|
|
22
|
+
*
|
|
23
|
+
* @param {string[]} [additionalIgnorePaths]
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export const resolveInvocationDir = ( additionalIgnorePaths = [] ) => {
|
|
27
|
+
const stack = new Error().stack;
|
|
28
|
+
const lines = stackTraceParser.parse( stack );
|
|
29
|
+
const ignorePaths = [ ...additionalIgnorePaths, ...defaultIgnorePaths ].map( transformSeparators );
|
|
30
|
+
|
|
31
|
+
const frame = lines.find( l => !ignorePaths.some( p => l.file.includes( p ) ) );
|
|
32
|
+
if ( !frame ) {
|
|
33
|
+
throw new Error( `Invocation dir resolution via stack trace failed. Stack: ${stack}` );
|
|
34
|
+
}
|
|
35
|
+
return frame.file.replace( 'file://', '' ).split( SEP ).slice( 0, -1 ).join( SEP );
|
|
36
|
+
};
|
package/src/llm/agent.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { ToolLoopAgent as AIToolLoopAgent } from 'ai';
|
|
2
|
+
import { loadAiSdkTextOptions } from './ai_sdk_options.js';
|
|
3
|
+
import { wrapTextGeneration, wrapStream } from './utils/wrap.js';
|
|
4
|
+
import { Role } from './consts.js';
|
|
5
|
+
import { drainStream } from './utils/stream.js';
|
|
6
|
+
import { loadPrompt } from './prompt/loader.js';
|
|
7
|
+
import { loadSkills } from './utils/skills.js';
|
|
8
|
+
import * as Validator from './validations.js';
|
|
9
|
+
import { Logger } from '#runtime_logger';
|
|
10
|
+
|
|
11
|
+
export class Agent {
|
|
12
|
+
#agent;
|
|
13
|
+
#prompt;
|
|
14
|
+
#initialMessages;
|
|
15
|
+
#store;
|
|
16
|
+
|
|
17
|
+
constructor( args ) {
|
|
18
|
+
const { promptFile, promptObject, promptDir, variables, tools, output, stopWhen, messageStore } = Validator.parseAgentArgs( args );
|
|
19
|
+
|
|
20
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
21
|
+
const skills = loadSkills( prompt );
|
|
22
|
+
|
|
23
|
+
const { instructions, messages, ...aiOptions } = loadAiSdkTextOptions( { prompt, skills, tools, output, stopWhen } );
|
|
24
|
+
|
|
25
|
+
this.#agent = new AIToolLoopAgent( {
|
|
26
|
+
...aiOptions,
|
|
27
|
+
...( instructions.length > 0 ? { instructions } : {} )
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
this.#prompt = prompt;
|
|
31
|
+
// `messages` is system-free but may still hold authored <assistant> blocks;
|
|
32
|
+
// seed only <user> turns into each generate()/stream() call.
|
|
33
|
+
this.#initialMessages = messages.filter( m => m.role === Role.USER );
|
|
34
|
+
this.#store = messageStore ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async #combineWithPreviousMessages( messages ) {
|
|
38
|
+
return [ ...this.#initialMessages, ...( await this.#store?.getMessages() ?? [] ), ...messages ];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async #storeMessages( messages ) {
|
|
42
|
+
if ( this.#store ) {
|
|
43
|
+
await this.#store.addMessages( messages );
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async generate( args ) {
|
|
48
|
+
const { messages, abortSignal, toolChoice } = Validator.parseAgentGenerateArgs( args );
|
|
49
|
+
const combinedMessages = await this.#combineWithPreviousMessages( messages );
|
|
50
|
+
|
|
51
|
+
return wrapTextGeneration( {
|
|
52
|
+
name: 'Agent.generate',
|
|
53
|
+
prompt: this.#prompt,
|
|
54
|
+
fn: async ( { onStepEndHook } ) => {
|
|
55
|
+
const response = await this.#agent.generate( {
|
|
56
|
+
messages: combinedMessages,
|
|
57
|
+
allowSystemInMessages: true,
|
|
58
|
+
...( abortSignal && { abortSignal } ),
|
|
59
|
+
...( toolChoice && { toolChoice } ),
|
|
60
|
+
onStepEnd: onStepEndHook
|
|
61
|
+
} );
|
|
62
|
+
if ( response.finishReason !== 'error' ) {
|
|
63
|
+
await this.#storeMessages( messages.concat( response.responseMessages ?? [] ) );
|
|
64
|
+
}
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
} );
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generates a completed agent response over streaming transport, invoking `onChunk` as parts arrive.
|
|
72
|
+
*/
|
|
73
|
+
async generateWithStreaming( args ) {
|
|
74
|
+
const { messages, abortSignal, toolChoice, onChunk } = Validator.parseAgentGenerateWithStreamingArgs( args );
|
|
75
|
+
const combinedMessages = await this.#combineWithPreviousMessages( messages );
|
|
76
|
+
|
|
77
|
+
return wrapTextGeneration( {
|
|
78
|
+
name: 'Agent.generateWithStreaming',
|
|
79
|
+
prompt: this.#prompt,
|
|
80
|
+
fn: async ( { onStepEndHook } ) => {
|
|
81
|
+
const state = { response: null };
|
|
82
|
+
const stream = await this.#agent.stream( {
|
|
83
|
+
messages: combinedMessages,
|
|
84
|
+
allowSystemInMessages: true,
|
|
85
|
+
...( onChunk && { onChunk } ),
|
|
86
|
+
...( abortSignal && { abortSignal } ),
|
|
87
|
+
...( toolChoice && { toolChoice } ),
|
|
88
|
+
onStepEnd: onStepEndHook,
|
|
89
|
+
onEnd: res => {
|
|
90
|
+
state.response = res;
|
|
91
|
+
},
|
|
92
|
+
onError: _ => {} // Suppress AI-SDK console printing
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
await drainStream( stream, abortSignal );
|
|
96
|
+
|
|
97
|
+
if ( !state.response ) {
|
|
98
|
+
throw new Error( 'Streaming completed without a response.' );
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
state.response.output = await stream.output;
|
|
102
|
+
if ( state.response.finishReason !== 'error' ) {
|
|
103
|
+
await this.#storeMessages( messages.concat( state.response.responseMessages ?? [] ) );
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return state.response;
|
|
107
|
+
}
|
|
108
|
+
} );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async stream( args ) {
|
|
112
|
+
const { messages, abortSignal, toolChoice, onChunk, onEnd, onError } = Validator.parseAgentStreamArgs( args );
|
|
113
|
+
const combinedMessages = await this.#combineWithPreviousMessages( messages );
|
|
114
|
+
|
|
115
|
+
return wrapStream( {
|
|
116
|
+
name: 'Agent.stream',
|
|
117
|
+
prompt: this.#prompt,
|
|
118
|
+
abortSignal,
|
|
119
|
+
fn: ( { onEndHook, onErrorHook, onStepEndHook, onAbortHook } ) => this.#agent.stream( {
|
|
120
|
+
messages: combinedMessages,
|
|
121
|
+
allowSystemInMessages: true,
|
|
122
|
+
...( onChunk && { onChunk } ),
|
|
123
|
+
...( abortSignal && { abortSignal } ),
|
|
124
|
+
...( toolChoice && { toolChoice } ),
|
|
125
|
+
onStepEnd: onStepEndHook,
|
|
126
|
+
onAbort: onAbortHook,
|
|
127
|
+
onEnd: response =>
|
|
128
|
+
onEndHook( response, async parsedResponse => {
|
|
129
|
+
if ( response.finishReason !== 'error' ) {
|
|
130
|
+
await this.#storeMessages( messages.concat( response.responseMessages ?? [] ) )
|
|
131
|
+
.catch( error =>
|
|
132
|
+
Logger.error( 'Message store persistence failed', { namespace: 'LLM', error: error?.message || String( error ) } )
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
await onEnd?.( parsedResponse );
|
|
136
|
+
} ),
|
|
137
|
+
onError: event => onErrorHook( event, error => onError?.( { ...event, error } ) )
|
|
138
|
+
} )
|
|
139
|
+
} );
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FatalError, ValidationError } from '#errors';
|
|
3
|
+
import { EnvHttpProxyAgent, fetch } from 'undici';
|
|
4
|
+
import { deprecatedProviderAliases } from './deprecated_provider_aliases.js';
|
|
5
|
+
import { resolveGoogleVertexAuthOptions } from './utils/google_vertex_auth.js';
|
|
6
|
+
|
|
7
|
+
/** This custom dispatcher has longer timeouts. */
|
|
8
|
+
const customDispatcher = new EnvHttpProxyAgent( {
|
|
9
|
+
headersTimeout: 15 * 60 * 1000, // 15 min
|
|
10
|
+
bodyTimeout: 15 * 60 * 1000,
|
|
11
|
+
allowH2: false // Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299
|
|
12
|
+
} );
|
|
13
|
+
|
|
14
|
+
/** This custom fetch instance uses the custom dispatcher */
|
|
15
|
+
const customFetch = ( input, init ) => fetch( input, { dispatcher: customDispatcher, ...init } );
|
|
16
|
+
|
|
17
|
+
/** Shipped providers. Their packages are optional peer dependencies, so any of them may be missing. */
|
|
18
|
+
const shippedProviders = {
|
|
19
|
+
'amazon-bedrock': {
|
|
20
|
+
pkg: '@ai-sdk/amazon-bedrock',
|
|
21
|
+
load: async () => ( await import( '@ai-sdk/amazon-bedrock' ) ).createAmazonBedrock
|
|
22
|
+
},
|
|
23
|
+
anthropic: {
|
|
24
|
+
pkg: '@ai-sdk/anthropic',
|
|
25
|
+
load: async () => ( await import( '@ai-sdk/anthropic' ) ).createAnthropic
|
|
26
|
+
},
|
|
27
|
+
azure: {
|
|
28
|
+
pkg: '@ai-sdk/azure',
|
|
29
|
+
load: async () => ( await import( '@ai-sdk/azure' ) ).createAzure
|
|
30
|
+
},
|
|
31
|
+
'google-vertex': {
|
|
32
|
+
pkg: '@ai-sdk/google-vertex',
|
|
33
|
+
load: async () => {
|
|
34
|
+
const { createVertex } = await import( '@ai-sdk/google-vertex' );
|
|
35
|
+
return options => createVertex( { ...options, ...resolveGoogleVertexAuthOptions() } );
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
openai: {
|
|
39
|
+
pkg: '@ai-sdk/openai',
|
|
40
|
+
load: async () => ( await import( '@ai-sdk/openai' ) ).createOpenAI
|
|
41
|
+
},
|
|
42
|
+
perplexity: {
|
|
43
|
+
pkg: '@ai-sdk/perplexity',
|
|
44
|
+
load: async () => ( await import( '@ai-sdk/perplexity' ) ).createPerplexity
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Load a shipped provider initializer, or null when its package is not installed. */
|
|
49
|
+
const loadInitializer = async ( { pkg, load } ) => {
|
|
50
|
+
try {
|
|
51
|
+
return await load();
|
|
52
|
+
} catch ( error ) {
|
|
53
|
+
if ( error?.code === 'ERR_MODULE_NOT_FOUND' && String( error.message ).includes( `'${pkg}'` ) ) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Available provider to initialize: the shipped providers whose package is installed. */
|
|
61
|
+
const providerInitializers = Object.fromEntries(
|
|
62
|
+
( await Promise.all( Object.entries( shippedProviders ).map( async ( [ name, provider ] ) => [ name, await loadInitializer( provider ) ] ) ) )
|
|
63
|
+
.filter( ( [ , initializer ] ) => initializer )
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
/** Providers already initialized due usage */
|
|
67
|
+
const initializedProviders = {};
|
|
68
|
+
|
|
69
|
+
/** Providers registered by the user */
|
|
70
|
+
const registeredProviders = {};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all available provider names: shipped providers whose package is installed, plus registered ones.
|
|
74
|
+
* @returns {string[]} Provider names
|
|
75
|
+
*/
|
|
76
|
+
export const getProviderNames = () =>
|
|
77
|
+
new Set( Object.keys( providerInitializers ).concat( Object.keys( registeredProviders ) ) ).values().toArray();
|
|
78
|
+
|
|
79
|
+
const registerProviderSchema = z.object( {
|
|
80
|
+
name: z.string().min( 1, 'Provider name must be a non-empty string' ),
|
|
81
|
+
providerFn: z.function()
|
|
82
|
+
} );
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Register or override an AI SDK provider factory by name.
|
|
86
|
+
* @param {string} name - Provider name used in prompt frontmatter
|
|
87
|
+
* @param {Function} providerFn - Factory function that receives a model id
|
|
88
|
+
* @returns {void}
|
|
89
|
+
*/
|
|
90
|
+
export const registerProvider = ( name, providerFn ) => {
|
|
91
|
+
const result = registerProviderSchema.safeParse( { name, providerFn } );
|
|
92
|
+
if ( !result.success ) {
|
|
93
|
+
throw new ValidationError( `Invalid provider registration: ${z.prettifyError( result.error )}` );
|
|
94
|
+
}
|
|
95
|
+
if ( Object.hasOwn( deprecatedProviderAliases, name ) ) {
|
|
96
|
+
const canonical = deprecatedProviderAliases[name];
|
|
97
|
+
throw new ValidationError(
|
|
98
|
+
`Cannot register provider "${name}": that name is a deprecated alias for "${canonical}". Register "${canonical}" instead.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
registeredProviders[name] = providerFn;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return a provider by its name.
|
|
106
|
+
* Look for registered providers first.
|
|
107
|
+
* If none, looks for initialized providers.
|
|
108
|
+
* Finally, looks for available provider initializers, and if found, init it.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} name
|
|
111
|
+
* @returns {object} provider
|
|
112
|
+
*/
|
|
113
|
+
export const getProvider = name => {
|
|
114
|
+
const provider = registeredProviders[name] ?? initializedProviders[name];
|
|
115
|
+
|
|
116
|
+
if ( provider ) {
|
|
117
|
+
return provider;
|
|
118
|
+
}
|
|
119
|
+
if ( providerInitializers[name] ) {
|
|
120
|
+
try {
|
|
121
|
+
return initializedProviders[name] = providerInitializers[name]( { fetch: customFetch } );
|
|
122
|
+
} catch ( error ) {
|
|
123
|
+
throw new FatalError( `Failed to initialize provider "${name}": ${error.message}`, { cause: error } );
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if ( shippedProviders[name] ) {
|
|
127
|
+
throw new FatalError( `Provider "${name}" requires the "${shippedProviders[name].pkg}" package, which is not installed` );
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new FatalError( `Unsupported provider "${name}"` );
|
|
131
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { loadImageModel, loadTextModel } from './utils/models.js';
|
|
2
|
+
import { buildLoadSkillTool, loadPromptTools } from './utils/tools.js';
|
|
3
|
+
import { Role } from './consts.js';
|
|
4
|
+
import { FatalError } from '#errors';
|
|
5
|
+
import { isStepCount } from 'ai';
|
|
6
|
+
|
|
7
|
+
const buildSkillsMessageContent = skills =>
|
|
8
|
+
'Available skills (use load_skill to get full instructions):\n' +
|
|
9
|
+
skills.map( s => `- ${s.name}: ${s.description}` ).join( '\n' );
|
|
10
|
+
|
|
11
|
+
const textPromptConfigKeys = [
|
|
12
|
+
'frequencyPenalty',
|
|
13
|
+
'maxOutputTokens',
|
|
14
|
+
'presencePenalty',
|
|
15
|
+
'providerOptions',
|
|
16
|
+
'seed',
|
|
17
|
+
'stopSequences',
|
|
18
|
+
'temperature',
|
|
19
|
+
'topK',
|
|
20
|
+
'topP'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const imagePromptConfigKeys = [
|
|
24
|
+
'aspectRatio',
|
|
25
|
+
'maxImagesPerCall',
|
|
26
|
+
'n',
|
|
27
|
+
'providerOptions',
|
|
28
|
+
'seed',
|
|
29
|
+
'size'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const select = ( keys, target ) =>
|
|
33
|
+
Object.fromEntries( Object.entries( target ).filter( ( [ k, v ] ) => keys.includes( k ) && v !== undefined ) );
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build options for AI SDK text generation.
|
|
37
|
+
*
|
|
38
|
+
* Splits system vs chat messages, applies prompt generation config, and merges tools
|
|
39
|
+
* with a step limit when any tools are present. Always sets `maxRetries` to 0.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} args
|
|
42
|
+
* @param {object} args.prompt - Loaded prompt object
|
|
43
|
+
* @param {object} [args.tools] - Caller tools
|
|
44
|
+
* @param {Skill[]} args.skills - Resolved skills
|
|
45
|
+
* @param {unknown} [args.stopWhen] - Caller stop condition; otherwise prompt `maxSteps` when tools exist
|
|
46
|
+
* @param {unknown} [args.output] - Structured output spec
|
|
47
|
+
* @param {unknown} [args.toolChoice] - Tool choice, set only when tools exist
|
|
48
|
+
* @param {AbortSignal} [args.abortSignal] - Abort signal for the request
|
|
49
|
+
* @returns {object} AI SDK text options
|
|
50
|
+
*/
|
|
51
|
+
export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output, toolChoice, abortSignal } ) => {
|
|
52
|
+
if ( prompt.messages.length === 0 ) {
|
|
53
|
+
throw new FatalError( `Prompt "${prompt.name}" has no chat-style messages. Add role-tagged blocks like <system> or <user>.` );
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const instructions = prompt.messages.filter( m => m.role === Role.SYSTEM );
|
|
57
|
+
const messages = prompt.messages.filter( m => m.role !== Role.SYSTEM );
|
|
58
|
+
|
|
59
|
+
if ( skills.length > 0 ) {
|
|
60
|
+
const skillsMessageContent = buildSkillsMessageContent( skills );
|
|
61
|
+
if ( instructions.length > 0 ) {
|
|
62
|
+
instructions[0] = { ...instructions[0], content: `${instructions[0].content}\n\n${skillsMessageContent}` };
|
|
63
|
+
} else {
|
|
64
|
+
instructions.push( { role: Role.SYSTEM, content: skillsMessageContent } );
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const options = {
|
|
69
|
+
allowSystemInMessages: true,
|
|
70
|
+
maxRetries: 0,
|
|
71
|
+
model: loadTextModel( prompt ),
|
|
72
|
+
instructions,
|
|
73
|
+
messages,
|
|
74
|
+
...( output && { output } ),
|
|
75
|
+
...( abortSignal && { abortSignal } ),
|
|
76
|
+
...( stopWhen && { stopWhen } ),
|
|
77
|
+
...select( textPromptConfigKeys, prompt.config )
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Tools parsing
|
|
81
|
+
const promptTools = loadPromptTools( prompt );
|
|
82
|
+
const skillsTools = skills.length > 0 ? { load_skill: buildLoadSkillTool( skills ) } : {};
|
|
83
|
+
const mergedTools = { ...promptTools, ...tools, ...skillsTools };
|
|
84
|
+
if ( Object.keys( mergedTools ).length > 0 ) {
|
|
85
|
+
options.tools = mergedTools;
|
|
86
|
+
if ( toolChoice ) {
|
|
87
|
+
options.toolChoice = toolChoice;
|
|
88
|
+
}
|
|
89
|
+
if ( !options.stopWhen ) {
|
|
90
|
+
options.stopWhen = isStepCount( prompt.config.maxSteps );
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return options;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build options for AI SDK image generation.
|
|
99
|
+
*
|
|
100
|
+
* Uses prompt instructions (with optional source images and mask) and prompt-owned
|
|
101
|
+
* image config (`n`, `size`, `aspectRatio`, `seed`, `maxImagesPerCall`, `providerOptions`).
|
|
102
|
+
* Always sets `maxRetries` to 0.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} args
|
|
105
|
+
* @param {object} args.prompt - Loaded prompt object
|
|
106
|
+
* @param {unknown} [args.images] - Source images for image-to-image
|
|
107
|
+
* @param {unknown} [args.mask] - Inpainting mask
|
|
108
|
+
* @param {AbortSignal} [args.abortSignal] - Abort signal for the request
|
|
109
|
+
* @returns {object} AI SDK image options
|
|
110
|
+
*/
|
|
111
|
+
export const loadAiSdkImageOptions = ( { prompt, images, mask, abortSignal } ) => {
|
|
112
|
+
if ( !prompt.instructions ) {
|
|
113
|
+
throw new FatalError( `Prompt "${prompt.name}" has no instructions. Image prompts must use plain instructions.` );
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
maxRetries: 0,
|
|
117
|
+
model: loadImageModel( prompt ),
|
|
118
|
+
prompt: ( images || mask ) ? {
|
|
119
|
+
text: prompt.instructions,
|
|
120
|
+
...( images && { images } ),
|
|
121
|
+
...( mask && { mask } )
|
|
122
|
+
} : prompt.instructions,
|
|
123
|
+
...( abortSignal && { abortSignal } ),
|
|
124
|
+
...select( imagePromptConfigKeys, prompt.config )
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as AI from 'ai';
|
|
2
|
+
import { loadPrompt } from './prompt/loader.js';
|
|
3
|
+
import { wrapImageGeneration, wrapStream, wrapTextGeneration } from './utils/wrap.js';
|
|
4
|
+
import { loadAiSdkTextOptions, loadAiSdkImageOptions } from './ai_sdk_options.js';
|
|
5
|
+
import { drainStream } from './utils/stream.js';
|
|
6
|
+
import { loadSkills } from './utils/skills.js';
|
|
7
|
+
import * as Validator from './validations.js';
|
|
8
|
+
|
|
9
|
+
export const generateText = async args => {
|
|
10
|
+
const { promptFile, promptObject, variables, promptDir, ...aiOptions } = Validator.parseGenerateTextArgs( args );
|
|
11
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
12
|
+
const skills = loadSkills( prompt );
|
|
13
|
+
|
|
14
|
+
return wrapTextGeneration( {
|
|
15
|
+
name: 'generateText',
|
|
16
|
+
prompt,
|
|
17
|
+
fn: ( { onStepEndHook } ) => AI.generateText( {
|
|
18
|
+
...loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ),
|
|
19
|
+
onStepEnd: onStepEndHook
|
|
20
|
+
} )
|
|
21
|
+
} );
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const streamText = args => {
|
|
25
|
+
const { promptFile, promptObject, variables, promptDir, onEnd, onError, onChunk, ...aiOptions } = Validator.parseStreamTextArgs( args );
|
|
26
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
27
|
+
const skills = loadSkills( prompt );
|
|
28
|
+
|
|
29
|
+
return wrapStream( {
|
|
30
|
+
name: 'streamText',
|
|
31
|
+
prompt,
|
|
32
|
+
abortSignal: aiOptions.abortSignal,
|
|
33
|
+
fn: ( { onEndHook, onErrorHook, onStepEndHook, onAbortHook } ) => AI.streamText( {
|
|
34
|
+
...loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ),
|
|
35
|
+
...( onChunk && { onChunk } ),
|
|
36
|
+
onStepEnd: onStepEndHook,
|
|
37
|
+
onAbort: onAbortHook,
|
|
38
|
+
onEnd: response => onEndHook( response, onEnd ),
|
|
39
|
+
onError: event => onErrorHook( event, error => onError?.( { ...event, error } ) )
|
|
40
|
+
} )
|
|
41
|
+
} );
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates a completed text response over streaming transport, invoking `onChunk` as parts arrive.
|
|
46
|
+
*/
|
|
47
|
+
export const generateTextWithStreaming = async args => {
|
|
48
|
+
const { promptFile, promptObject, variables, promptDir, onChunk, ...aiOptions } = Validator.parseGenerateTextWithStreamingArgs( args );
|
|
49
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
50
|
+
const skills = loadSkills( prompt );
|
|
51
|
+
|
|
52
|
+
return wrapTextGeneration( {
|
|
53
|
+
name: 'generateTextWithStreaming',
|
|
54
|
+
prompt,
|
|
55
|
+
fn: async ( { onStepEndHook } ) => {
|
|
56
|
+
const state = { response: null };
|
|
57
|
+
const stream = AI.streamText( {
|
|
58
|
+
...loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ),
|
|
59
|
+
...( onChunk && { onChunk } ),
|
|
60
|
+
onStepEnd: onStepEndHook,
|
|
61
|
+
onEnd: res => {
|
|
62
|
+
state.response = res;
|
|
63
|
+
},
|
|
64
|
+
onError: _ => {} // Suppress AI-SDK console printing
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
await drainStream( stream, aiOptions.abortSignal );
|
|
68
|
+
|
|
69
|
+
if ( !state.response ) {
|
|
70
|
+
throw new Error( 'Streaming completed without a response.' );
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
state.response.output = await stream.output;
|
|
74
|
+
return state.response;
|
|
75
|
+
}
|
|
76
|
+
} );
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const generateImage = async args => {
|
|
80
|
+
const { promptFile, promptObject, promptDir, variables, ...aiOptions } = Validator.parseGenerateImageArgs( args );
|
|
81
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
82
|
+
|
|
83
|
+
return wrapImageGeneration( {
|
|
84
|
+
name: 'generateImage',
|
|
85
|
+
prompt,
|
|
86
|
+
fn: () => AI.generateImage( loadAiSdkImageOptions( { prompt, ...aiOptions } ) )
|
|
87
|
+
} );
|
|
88
|
+
};
|