@outputai/llm 0.11.0 → 0.11.1-next.2223fa5.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 +2 -2
- package/src/agent.js +105 -83
- package/src/agent.spec.js +312 -225
- package/src/ai_provider.js +2 -2
- package/src/ai_sdk_options.js +93 -39
- package/src/ai_sdk_options.spec.js +314 -154
- package/src/consts.js +6 -0
- package/src/generate.js +81 -0
- package/src/generate.spec.js +466 -0
- package/src/index.d.ts +222 -242
- package/src/index.js +3 -4
- package/src/prompt/content.js +70 -0
- package/src/prompt/content.spec.js +208 -0
- package/src/prompt/{escape.js → interpolations.js} +11 -18
- package/src/prompt/interpolations.spec.js +109 -0
- package/src/prompt/loader.full.spec.js +293 -0
- package/src/prompt/loader.js +45 -39
- package/src/prompt/loader.spec.js +186 -289
- package/src/prompt/markup/attributes.js +52 -0
- package/src/prompt/markup/attributes.spec.js +132 -0
- package/src/prompt/markup/nodes.js +71 -0
- package/src/prompt/markup/nodes.spec.js +333 -0
- package/src/prompt/markup/tokenizer.js +3 -0
- package/src/prompt/markup/tokenizer.spec.js +149 -0
- package/src/prompt/markup/tokens.js +26 -0
- package/src/prompt/markup/tokens.spec.js +250 -0
- package/src/prompt/validations.js +98 -67
- package/src/prompt/validations.spec.js +204 -47
- package/src/{prompt/load_content.js → utils/file.js} +12 -15
- package/src/utils/file.spec.js +89 -0
- package/src/utils/models.js +15 -0
- package/src/utils/models.spec.js +119 -0
- package/src/utils/skills.js +74 -0
- package/src/utils/skills.spec.js +168 -0
- package/src/utils/sources.js +48 -0
- package/src/utils/sources.spec.js +122 -0
- package/src/utils/stream.js +22 -0
- package/src/utils/stream.spec.js +55 -0
- package/src/utils/tools.js +47 -0
- package/src/utils/tools.spec.js +167 -0
- package/src/utils/wrap.js +148 -0
- package/src/utils/wrap.spec.js +359 -0
- package/src/validations.js +149 -27
- package/src/validations.spec.js +451 -53
- package/src/ai_model.js +0 -60
- package/src/ai_model.spec.js +0 -259
- package/src/ai_sdk.js +0 -92
- package/src/ai_sdk.spec.js +0 -564
- package/src/prompt/block_options.js +0 -58
- package/src/prompt/block_options.spec.js +0 -71
- package/src/prompt/blocks.js +0 -47
- package/src/prompt/blocks.spec.js +0 -63
- package/src/prompt/escape.spec.js +0 -159
- package/src/prompt/load_content.spec.js +0 -83
- package/src/prompt/loader_validation.spec.js +0 -128
- package/src/prompt/parser.js +0 -16
- package/src/prompt/parser.spec.js +0 -186
- package/src/prompt/prepare_text.js +0 -27
- package/src/prompt/prepare_text.spec.js +0 -141
- package/src/prompt/skill.js +0 -128
- package/src/prompt/skill.spec.js +0 -172
- package/src/utils/message.js +0 -3
- package/src/utils/message.spec.js +0 -29
- package/src/utils/response_wrappers.js +0 -100
- package/src/utils/response_wrappers.spec.js +0 -240
- package/src/utils/source_extraction.js +0 -53
- package/src/utils/source_extraction.spec.js +0 -194
- package/src/utils/trace.js +0 -19
- package/src/utils/trace.spec.js +0 -112
|
@@ -2,12 +2,21 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
|
|
3
3
|
const loadModelImpl = vi.fn();
|
|
4
4
|
const loadImageModelImpl = vi.fn();
|
|
5
|
-
const
|
|
5
|
+
const loadPromptToolsImpl = vi.fn();
|
|
6
|
+
const buildLoadSkillToolImpl = vi.fn();
|
|
6
7
|
|
|
7
|
-
vi.mock( './
|
|
8
|
+
vi.mock( './utils/models.js', () => ( {
|
|
8
9
|
loadTextModel: ( ...args ) => loadModelImpl( ...args ),
|
|
9
|
-
loadImageModel: ( ...args ) => loadImageModelImpl( ...args )
|
|
10
|
-
|
|
10
|
+
loadImageModel: ( ...args ) => loadImageModelImpl( ...args )
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
vi.mock( './utils/tools.js', () => ( {
|
|
14
|
+
buildLoadSkillTool: ( ...args ) => buildLoadSkillToolImpl( ...args ),
|
|
15
|
+
loadPromptTools: ( ...args ) => loadPromptToolsImpl( ...args )
|
|
16
|
+
} ) );
|
|
17
|
+
|
|
18
|
+
vi.mock( 'ai', () => ( {
|
|
19
|
+
stepCountIs: count => ( { type: 'step-count', count } )
|
|
11
20
|
} ) );
|
|
12
21
|
|
|
13
22
|
const importSut = async () => import( './ai_sdk_options.js' );
|
|
@@ -17,6 +26,7 @@ const makeTextPrompt = config => ( {
|
|
|
17
26
|
config: {
|
|
18
27
|
provider: 'anthropic',
|
|
19
28
|
model: 'claude-haiku-4-5',
|
|
29
|
+
maxSteps: 10,
|
|
20
30
|
...config
|
|
21
31
|
},
|
|
22
32
|
messages: [
|
|
@@ -37,194 +47,344 @@ const makeImagePrompt = config => ( {
|
|
|
37
47
|
instructions: 'Generate a cinematic image of a NASCAR race at sunset.'
|
|
38
48
|
} );
|
|
39
49
|
|
|
50
|
+
const writerSkill = {
|
|
51
|
+
name: 'writer',
|
|
52
|
+
description: 'Writes copy',
|
|
53
|
+
instructions: 'Do it.'
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const skillsCatalog =
|
|
57
|
+
'Available skills (use load_skill to get full instructions):\n- writer: Writes copy';
|
|
58
|
+
|
|
40
59
|
describe( 'ai_sdk_options', () => {
|
|
41
60
|
beforeEach( () => {
|
|
42
61
|
vi.resetModules();
|
|
43
62
|
vi.clearAllMocks();
|
|
44
63
|
loadModelImpl.mockReturnValue( 'MODEL' );
|
|
45
64
|
loadImageModelImpl.mockReturnValue( 'IMAGE_MODEL' );
|
|
46
|
-
|
|
65
|
+
loadPromptToolsImpl.mockReturnValue( null );
|
|
66
|
+
buildLoadSkillToolImpl.mockImplementation( skills => ( { type: 'load_skill', skills } ) );
|
|
47
67
|
} );
|
|
48
68
|
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
describe( 'loadAiSdkTextOptions', () => {
|
|
70
|
+
const loadText = async args => {
|
|
71
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
72
|
+
return loadAiSdkTextOptions( { skills: [], ...args } );
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
it( 'maps a loaded prompt to model, system, messages, and generation config', async () => {
|
|
76
|
+
const prompt = makeTextPrompt( {
|
|
77
|
+
temperature: 0.3,
|
|
78
|
+
maxOutputTokens: 1000,
|
|
79
|
+
providerOptions: { anthropic: { effort: 'medium' } }
|
|
80
|
+
} );
|
|
81
|
+
|
|
82
|
+
const result = await loadText( { prompt } );
|
|
83
|
+
|
|
84
|
+
expect( loadModelImpl ).toHaveBeenCalledWith( prompt );
|
|
85
|
+
expect( loadPromptToolsImpl ).toHaveBeenCalledWith( prompt );
|
|
86
|
+
expect( result ).toEqual( {
|
|
87
|
+
allowSystemInMessages: true,
|
|
88
|
+
maxRetries: 0,
|
|
89
|
+
model: 'MODEL',
|
|
90
|
+
system: [ { role: 'system', content: 'You are concise.' } ],
|
|
91
|
+
messages: [ { role: 'user', content: 'Hello' } ],
|
|
92
|
+
providerOptions: prompt.config.providerOptions,
|
|
93
|
+
temperature: 0.3,
|
|
94
|
+
maxOutputTokens: 1000
|
|
95
|
+
} );
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
it( 'forwards all supported text generation options', async () => {
|
|
99
|
+
const generationOptions = {
|
|
100
|
+
frequencyPenalty: 0.2,
|
|
101
|
+
maxOutputTokens: 2000,
|
|
102
|
+
presencePenalty: 0.3,
|
|
103
|
+
providerOptions: { openai: { reasoningEffort: 'low' } },
|
|
104
|
+
seed: 42,
|
|
105
|
+
stopSequences: [ 'END' ],
|
|
106
|
+
temperature: 0.4,
|
|
107
|
+
topK: 40,
|
|
108
|
+
topP: 0.9
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = await loadText( { prompt: makeTextPrompt( generationOptions ) } );
|
|
112
|
+
|
|
113
|
+
expect( result ).toMatchObject( generationOptions );
|
|
54
114
|
} );
|
|
55
115
|
|
|
56
|
-
|
|
57
|
-
|
|
116
|
+
it( 'preserves temperature 0', async () => {
|
|
117
|
+
const result = await loadText( { prompt: makeTextPrompt( { temperature: 0 } ) } );
|
|
58
118
|
|
|
59
|
-
|
|
60
|
-
expect( loadToolsImpl ).toHaveBeenCalledWith( prompt );
|
|
61
|
-
expect( result ).toEqual( {
|
|
62
|
-
model: 'MODEL',
|
|
63
|
-
system: [ { role: 'system', content: 'You are concise.' } ],
|
|
64
|
-
messages: [ { role: 'user', content: 'Hello' } ],
|
|
65
|
-
providerOptions: prompt.config.providerOptions,
|
|
66
|
-
temperature: 0.3,
|
|
67
|
-
maxOutputTokens: 1000
|
|
119
|
+
expect( result.temperature ).toBe( 0 );
|
|
68
120
|
} );
|
|
69
|
-
} );
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
|
|
122
|
+
it( 'throws when the prompt has no chat-style messages', async () => {
|
|
123
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
expect( () => loadAiSdkTextOptions( { prompt: makeImagePrompt(), skills: [] } ) ).toThrow(
|
|
126
|
+
'Prompt "image@v1" has no chat-style messages.'
|
|
127
|
+
);
|
|
128
|
+
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
129
|
+
expect( loadPromptToolsImpl ).not.toHaveBeenCalled();
|
|
130
|
+
} );
|
|
76
131
|
|
|
77
|
-
|
|
78
|
-
|
|
132
|
+
it( 'forwards per-message providerOptions when splitting system and messages', async () => {
|
|
133
|
+
const providerOptions = { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } } };
|
|
134
|
+
const prompt = {
|
|
135
|
+
name: 'cache@v1',
|
|
136
|
+
config: {
|
|
137
|
+
provider: 'anthropic',
|
|
138
|
+
model: 'claude-sonnet-4-5'
|
|
139
|
+
},
|
|
140
|
+
messages: [
|
|
141
|
+
{ role: 'system', content: 'Static', providerOptions },
|
|
142
|
+
{ role: 'user', content: 'Hello' }
|
|
143
|
+
],
|
|
144
|
+
instructions: null
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = await loadText( { prompt } );
|
|
148
|
+
|
|
149
|
+
expect( result.system ).toEqual( [ {
|
|
150
|
+
role: 'system',
|
|
151
|
+
content: 'Static',
|
|
152
|
+
providerOptions
|
|
153
|
+
} ] );
|
|
154
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
155
|
+
} );
|
|
79
156
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
157
|
+
it( 'returns an empty system array when the prompt has no system block', async () => {
|
|
158
|
+
const prompt = {
|
|
159
|
+
name: 'no-system@v1',
|
|
160
|
+
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
161
|
+
messages: [ { role: 'user', content: 'Hello' } ],
|
|
162
|
+
instructions: null
|
|
163
|
+
};
|
|
84
164
|
|
|
85
|
-
|
|
86
|
-
const result = loadAiSdkTextOptions( prompt );
|
|
165
|
+
const result = await loadText( { prompt } );
|
|
87
166
|
|
|
88
|
-
|
|
89
|
-
|
|
167
|
+
expect( result.system ).toEqual( [] );
|
|
168
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
169
|
+
} );
|
|
90
170
|
|
|
91
|
-
|
|
92
|
-
|
|
171
|
+
it( 'groups multiple system blocks into system and keeps them out of messages', async () => {
|
|
172
|
+
const prompt = {
|
|
173
|
+
name: 'multi-system@v1',
|
|
174
|
+
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
175
|
+
messages: [
|
|
176
|
+
{ role: 'system', content: 'First' },
|
|
177
|
+
{ role: 'system', content: 'Second' },
|
|
178
|
+
{ role: 'user', content: 'Hello' }
|
|
179
|
+
],
|
|
180
|
+
instructions: null
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = await loadText( { prompt } );
|
|
184
|
+
|
|
185
|
+
expect( result.system ).toEqual( [
|
|
186
|
+
{ role: 'system', content: 'First' },
|
|
187
|
+
{ role: 'system', content: 'Second' }
|
|
188
|
+
] );
|
|
189
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
190
|
+
} );
|
|
93
191
|
|
|
94
|
-
|
|
192
|
+
it( 'omits tools and stopWhen when no tools are present', async () => {
|
|
193
|
+
const result = await loadText( { prompt: makeTextPrompt() } );
|
|
95
194
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
expect( loadToolsImpl ).not.toHaveBeenCalled();
|
|
101
|
-
} );
|
|
195
|
+
expect( result.tools ).toBeUndefined();
|
|
196
|
+
expect( result.stopWhen ).toBeUndefined();
|
|
197
|
+
expect( buildLoadSkillToolImpl ).not.toHaveBeenCalled();
|
|
198
|
+
} );
|
|
102
199
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const prompt = makeImagePrompt( {
|
|
107
|
-
n: 2,
|
|
108
|
-
maxImagesPerCall: 1,
|
|
109
|
-
size: '1024x1024',
|
|
110
|
-
aspectRatio: '1:1',
|
|
111
|
-
seed: 42,
|
|
112
|
-
temperature: 0.7,
|
|
113
|
-
maxTokens: 1000,
|
|
114
|
-
providerOptions: { openai: { quality: 'high' } }
|
|
115
|
-
} );
|
|
116
|
-
|
|
117
|
-
const { loadAiSdkImageOptions } = await importSut();
|
|
118
|
-
const result = loadAiSdkImageOptions( { prompt, images, mask } );
|
|
119
|
-
|
|
120
|
-
expect( loadImageModelImpl ).toHaveBeenCalledWith( prompt );
|
|
121
|
-
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
122
|
-
expect( loadToolsImpl ).not.toHaveBeenCalled();
|
|
123
|
-
expect( result ).toEqual( {
|
|
124
|
-
model: 'IMAGE_MODEL',
|
|
125
|
-
prompt: {
|
|
126
|
-
text: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
127
|
-
images,
|
|
128
|
-
mask
|
|
129
|
-
},
|
|
130
|
-
providerOptions: prompt.config.providerOptions,
|
|
131
|
-
n: 2,
|
|
132
|
-
maxImagesPerCall: 1,
|
|
133
|
-
size: '1024x1024',
|
|
134
|
-
aspectRatio: '1:1',
|
|
135
|
-
seed: 42
|
|
136
|
-
} );
|
|
137
|
-
expect( result.temperature ).toBeUndefined();
|
|
138
|
-
expect( result.maxOutputTokens ).toBeUndefined();
|
|
139
|
-
} );
|
|
200
|
+
it( 'sets prompt tools and stopWhen from maxSteps', async () => {
|
|
201
|
+
const promptTools = { googleSearch: { type: 'google-search-tool' } };
|
|
202
|
+
loadPromptToolsImpl.mockReturnValue( promptTools );
|
|
140
203
|
|
|
141
|
-
|
|
142
|
-
const prompt = makeImagePrompt( { seed: 0 } );
|
|
204
|
+
const result = await loadText( { prompt: makeTextPrompt( { tools: { googleSearch: {} }, maxSteps: 4 } ) } );
|
|
143
205
|
|
|
144
|
-
|
|
145
|
-
|
|
206
|
+
expect( result.tools ).toEqual( promptTools );
|
|
207
|
+
expect( result.stopWhen ).toEqual( { type: 'step-count', count: 4 } );
|
|
208
|
+
} );
|
|
146
209
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
210
|
+
it( 'lets caller tools override prompt tools on the same key', async () => {
|
|
211
|
+
loadPromptToolsImpl.mockReturnValue( {
|
|
212
|
+
googleSearch: { from: 'prompt' },
|
|
213
|
+
urlContext: { from: 'prompt' }
|
|
214
|
+
} );
|
|
215
|
+
|
|
216
|
+
const result = await loadText( {
|
|
217
|
+
prompt: makeTextPrompt(),
|
|
218
|
+
tools: { googleSearch: { from: 'caller' } }
|
|
219
|
+
} );
|
|
220
|
+
|
|
221
|
+
expect( result.tools ).toEqual( {
|
|
222
|
+
googleSearch: { from: 'caller' },
|
|
223
|
+
urlContext: { from: 'prompt' }
|
|
224
|
+
} );
|
|
152
225
|
} );
|
|
153
|
-
} );
|
|
154
226
|
|
|
155
|
-
|
|
156
|
-
|
|
227
|
+
it( 'appends the skills catalog to the first system message and adds load_skill', async () => {
|
|
228
|
+
const providerOptions = { anthropic: { cacheControl: { type: 'ephemeral' } } };
|
|
229
|
+
const prompt = makeTextPrompt();
|
|
230
|
+
prompt.messages[0] = { ...prompt.messages[0], providerOptions };
|
|
231
|
+
|
|
232
|
+
const result = await loadText( {
|
|
233
|
+
prompt,
|
|
234
|
+
skills: [ writerSkill ]
|
|
235
|
+
} );
|
|
236
|
+
|
|
237
|
+
expect( buildLoadSkillToolImpl ).toHaveBeenCalledWith( [ writerSkill ] );
|
|
238
|
+
expect( result.system ).toEqual( [
|
|
239
|
+
{ role: 'system', content: `You are concise.\n\n${skillsCatalog}`, providerOptions }
|
|
240
|
+
] );
|
|
241
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
242
|
+
expect( result.tools ).toEqual( {
|
|
243
|
+
load_skill: { type: 'load_skill', skills: [ writerSkill ] }
|
|
244
|
+
} );
|
|
245
|
+
expect( result.stopWhen ).toEqual( { type: 'step-count', count: 10 } );
|
|
246
|
+
} );
|
|
157
247
|
|
|
158
|
-
|
|
248
|
+
it( 'adds a system message for the skills catalog when none exists', async () => {
|
|
249
|
+
const prompt = {
|
|
250
|
+
name: 'no-system@v1',
|
|
251
|
+
config: { provider: 'anthropic', model: 'claude-haiku-4-5', maxSteps: 10 },
|
|
252
|
+
messages: [ { role: 'user', content: 'Hello' } ],
|
|
253
|
+
instructions: null
|
|
254
|
+
};
|
|
159
255
|
|
|
160
|
-
|
|
161
|
-
'Prompt "test@v1" has no instructions.'
|
|
162
|
-
);
|
|
163
|
-
expect( loadImageModelImpl ).not.toHaveBeenCalled();
|
|
164
|
-
} );
|
|
256
|
+
const result = await loadText( { prompt, skills: [ writerSkill ] } );
|
|
165
257
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
config: {
|
|
170
|
-
provider: 'anthropic',
|
|
171
|
-
model: 'claude-sonnet-4-5',
|
|
172
|
-
messageOptions: { cached: { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } } } }
|
|
173
|
-
},
|
|
174
|
-
messages: [
|
|
175
|
-
{ role: 'system', content: 'Static', attributes: { options: 'cached' } },
|
|
176
|
-
{ role: 'user', content: 'Hello' }
|
|
177
|
-
],
|
|
178
|
-
instructions: null
|
|
179
|
-
};
|
|
258
|
+
expect( result.system ).toEqual( [ { role: 'system', content: skillsCatalog } ] );
|
|
259
|
+
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
260
|
+
} );
|
|
180
261
|
|
|
181
|
-
|
|
182
|
-
|
|
262
|
+
it( 'keeps load_skill when the caller uses the same key', async () => {
|
|
263
|
+
const result = await loadText( {
|
|
264
|
+
prompt: makeTextPrompt(),
|
|
265
|
+
tools: { load_skill: { from: 'caller' }, search: { from: 'caller' } },
|
|
266
|
+
skills: [ writerSkill ]
|
|
267
|
+
} );
|
|
268
|
+
|
|
269
|
+
expect( result.tools ).toEqual( {
|
|
270
|
+
load_skill: { type: 'load_skill', skills: [ writerSkill ] },
|
|
271
|
+
search: { from: 'caller' }
|
|
272
|
+
} );
|
|
273
|
+
} );
|
|
183
274
|
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
275
|
+
it( 'forwards output, abortSignal, and caller stopWhen', async () => {
|
|
276
|
+
const output = { type: 'object' };
|
|
277
|
+
const abortSignal = AbortSignal.abort();
|
|
278
|
+
const stopWhen = { type: 'custom-stop' };
|
|
279
|
+
|
|
280
|
+
const result = await loadText( {
|
|
281
|
+
prompt: makeTextPrompt(),
|
|
282
|
+
output,
|
|
283
|
+
abortSignal,
|
|
284
|
+
stopWhen
|
|
285
|
+
} );
|
|
286
|
+
|
|
287
|
+
expect( result.output ).toBe( output );
|
|
288
|
+
expect( result.abortSignal ).toBe( abortSignal );
|
|
289
|
+
expect( result.stopWhen ).toBe( stopWhen );
|
|
290
|
+
} );
|
|
193
291
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
198
|
-
messages: [ { role: 'user', content: 'Hello' } ],
|
|
199
|
-
instructions: null
|
|
200
|
-
};
|
|
292
|
+
it( 'does not replace caller stopWhen with maxSteps when tools are present', async () => {
|
|
293
|
+
loadPromptToolsImpl.mockReturnValue( { googleSearch: { type: 'google-search-tool' } } );
|
|
294
|
+
const stopWhen = { type: 'custom-stop' };
|
|
201
295
|
|
|
202
|
-
|
|
203
|
-
|
|
296
|
+
const result = await loadText( {
|
|
297
|
+
prompt: makeTextPrompt( { maxSteps: 4 } ),
|
|
298
|
+
stopWhen
|
|
299
|
+
} );
|
|
204
300
|
|
|
205
|
-
|
|
206
|
-
|
|
301
|
+
expect( result.stopWhen ).toBe( stopWhen );
|
|
302
|
+
} );
|
|
303
|
+
|
|
304
|
+
it( 'sets toolChoice only when tools are present', async () => {
|
|
305
|
+
const withoutTools = await loadText( {
|
|
306
|
+
prompt: makeTextPrompt(),
|
|
307
|
+
toolChoice: 'required'
|
|
308
|
+
} );
|
|
309
|
+
expect( withoutTools.toolChoice ).toBeUndefined();
|
|
310
|
+
|
|
311
|
+
loadPromptToolsImpl.mockReturnValue( { googleSearch: { type: 'google-search-tool' } } );
|
|
312
|
+
const withTools = await loadText( {
|
|
313
|
+
prompt: makeTextPrompt(),
|
|
314
|
+
toolChoice: 'required'
|
|
315
|
+
} );
|
|
316
|
+
expect( withTools.toolChoice ).toBe( 'required' );
|
|
317
|
+
} );
|
|
207
318
|
} );
|
|
208
319
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
320
|
+
describe( 'loadAiSdkImageOptions', () => {
|
|
321
|
+
it( 'maps a loaded prompt to image options and ignores text generation config', async () => {
|
|
322
|
+
const images = [ Buffer.from( 'image-bytes' ) ];
|
|
323
|
+
const mask = Buffer.from( 'mask-bytes' );
|
|
324
|
+
const prompt = makeImagePrompt( {
|
|
325
|
+
n: 2,
|
|
326
|
+
maxImagesPerCall: 1,
|
|
327
|
+
size: '1024x1024',
|
|
328
|
+
aspectRatio: '1:1',
|
|
329
|
+
seed: 42,
|
|
330
|
+
temperature: 0.7,
|
|
331
|
+
maxOutputTokens: 1000,
|
|
332
|
+
providerOptions: { openai: { quality: 'high' } }
|
|
333
|
+
} );
|
|
334
|
+
|
|
335
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
336
|
+
const result = loadAiSdkImageOptions( { prompt, images, mask } );
|
|
337
|
+
|
|
338
|
+
expect( loadImageModelImpl ).toHaveBeenCalledWith( prompt );
|
|
339
|
+
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
340
|
+
expect( loadPromptToolsImpl ).not.toHaveBeenCalled();
|
|
341
|
+
expect( result ).toEqual( {
|
|
342
|
+
maxRetries: 0,
|
|
343
|
+
model: 'IMAGE_MODEL',
|
|
344
|
+
prompt: {
|
|
345
|
+
text: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
346
|
+
images,
|
|
347
|
+
mask
|
|
348
|
+
},
|
|
349
|
+
providerOptions: prompt.config.providerOptions,
|
|
350
|
+
n: 2,
|
|
351
|
+
maxImagesPerCall: 1,
|
|
352
|
+
size: '1024x1024',
|
|
353
|
+
aspectRatio: '1:1',
|
|
354
|
+
seed: 42
|
|
355
|
+
} );
|
|
356
|
+
expect( result.temperature ).toBeUndefined();
|
|
357
|
+
expect( result.maxOutputTokens ).toBeUndefined();
|
|
358
|
+
} );
|
|
220
359
|
|
|
221
|
-
|
|
222
|
-
|
|
360
|
+
it( 'omits undefined image options while preserving explicit 0 seed', async () => {
|
|
361
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
362
|
+
const result = loadAiSdkImageOptions( { prompt: makeImagePrompt( { seed: 0 } ) } );
|
|
363
|
+
|
|
364
|
+
expect( result ).toEqual( {
|
|
365
|
+
maxRetries: 0,
|
|
366
|
+
model: 'IMAGE_MODEL',
|
|
367
|
+
prompt: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
368
|
+
providerOptions: undefined,
|
|
369
|
+
seed: 0
|
|
370
|
+
} );
|
|
371
|
+
} );
|
|
223
372
|
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
373
|
+
it( 'throws when the prompt has no instructions', async () => {
|
|
374
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
375
|
+
|
|
376
|
+
expect( () => loadAiSdkImageOptions( { prompt: makeTextPrompt() } ) ).toThrow(
|
|
377
|
+
'Prompt "test@v1" has no instructions.'
|
|
378
|
+
);
|
|
379
|
+
expect( loadImageModelImpl ).not.toHaveBeenCalled();
|
|
380
|
+
} );
|
|
381
|
+
|
|
382
|
+
it( 'forwards abortSignal', async () => {
|
|
383
|
+
const abortSignal = AbortSignal.abort();
|
|
384
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
385
|
+
const result = loadAiSdkImageOptions( { prompt: makeImagePrompt(), abortSignal } );
|
|
386
|
+
|
|
387
|
+
expect( result.abortSignal ).toBe( abortSignal );
|
|
388
|
+
} );
|
|
229
389
|
} );
|
|
230
390
|
} );
|
package/src/consts.js
ADDED
package/src/generate.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as AI from 'ai';
|
|
2
|
+
import { loadPrompt } from './prompt/loader.js';
|
|
3
|
+
import { wrapGeneration, wrapStream } 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, variables, promptDir, ...aiOptions } = Validator.parseGenerateTextArgs( args );
|
|
11
|
+
const prompt = loadPrompt( promptFile, variables, promptDir );
|
|
12
|
+
const skills = loadSkills( prompt );
|
|
13
|
+
|
|
14
|
+
return wrapGeneration( {
|
|
15
|
+
name: 'generateText',
|
|
16
|
+
prompt,
|
|
17
|
+
fn: () => AI.generateText( loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ) )
|
|
18
|
+
} );
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const streamText = args => {
|
|
22
|
+
const { promptFile, variables, promptDir, onFinish, onError, onChunk, ...aiOptions } = Validator.parseStreamTextArgs( args );
|
|
23
|
+
const prompt = loadPrompt( promptFile, variables, promptDir );
|
|
24
|
+
const skills = loadSkills( prompt );
|
|
25
|
+
|
|
26
|
+
return wrapStream( {
|
|
27
|
+
name: 'streamText',
|
|
28
|
+
prompt,
|
|
29
|
+
fn: ( { onFinishHook, onErrorHook } ) => AI.streamText( {
|
|
30
|
+
...loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ),
|
|
31
|
+
...( onChunk && { onChunk } ),
|
|
32
|
+
onFinish: response => onFinishHook( response, onFinish ),
|
|
33
|
+
onError: event => onErrorHook( event, error => onError?.( { ...event, error } ) )
|
|
34
|
+
} )
|
|
35
|
+
} );
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates a completed text response over streaming transport, invoking `onChunk` as parts arrive.
|
|
40
|
+
*/
|
|
41
|
+
export const generateTextWithStreaming = async args => {
|
|
42
|
+
const { promptFile, variables, promptDir, onChunk, ...aiOptions } = Validator.parseGenerateTextWithStreamingArgs( args );
|
|
43
|
+
const prompt = loadPrompt( promptFile, variables, promptDir );
|
|
44
|
+
const skills = loadSkills( prompt );
|
|
45
|
+
|
|
46
|
+
return wrapGeneration( {
|
|
47
|
+
name: 'generateTextWithStreaming',
|
|
48
|
+
prompt,
|
|
49
|
+
fn: async () => {
|
|
50
|
+
const state = { response: null };
|
|
51
|
+
const stream = AI.streamText( {
|
|
52
|
+
...loadAiSdkTextOptions( { prompt, skills, ...aiOptions } ),
|
|
53
|
+
...( onChunk && { onChunk } ),
|
|
54
|
+
onFinish: res => {
|
|
55
|
+
state.response = res;
|
|
56
|
+
},
|
|
57
|
+
onError: _ => {} // Suppress AI-SDK console printing
|
|
58
|
+
} );
|
|
59
|
+
|
|
60
|
+
await drainStream( stream, aiOptions.abortSignal );
|
|
61
|
+
|
|
62
|
+
if ( !state.response ) {
|
|
63
|
+
throw new Error( 'Streaming generation completed without a response.' );
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
state.response.output = await stream.output;
|
|
67
|
+
return state.response;
|
|
68
|
+
}
|
|
69
|
+
} );
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const generateImage = async args => {
|
|
73
|
+
const { promptFile, promptDir, variables, ...aiOptions } = Validator.parseGenerateImageArgs( args );
|
|
74
|
+
const prompt = loadPrompt( promptFile, variables, promptDir );
|
|
75
|
+
|
|
76
|
+
return wrapGeneration( {
|
|
77
|
+
name: 'generateImage',
|
|
78
|
+
prompt,
|
|
79
|
+
fn: () => AI.generateImage( loadAiSdkImageOptions( { prompt, ...aiOptions } ) )
|
|
80
|
+
} );
|
|
81
|
+
};
|