@outputai/llm 0.11.1-next.b3beef1.0 → 0.11.1-next.b4488bf.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 +17 -16
- package/src/agent.js +18 -19
- package/src/agent.spec.js +44 -27
- package/src/ai_sdk_options.js +37 -21
- package/src/ai_sdk_options.spec.js +30 -12
- package/src/fixtures/image_response_v7_google_vertex.js +72 -0
- package/src/fixtures/image_response_v7_openai.js +77 -0
- package/src/fixtures/models_api_v7_subset.json +259 -0
- package/src/fixtures/stream_response_v7_anthropic.js +389 -0
- package/src/fixtures/stream_response_v7_google_vertex.js +462 -0
- package/src/fixtures/stream_response_v7_openai.js +353 -0
- package/src/fixtures/stream_response_v7_perplexity.js +671 -0
- package/src/fixtures/text_response_v7_anthropic.js +359 -0
- package/src/fixtures/text_response_v7_anthropic_cache.js +719 -0
- package/src/fixtures/text_response_v7_google_vertex.js +435 -0
- package/src/fixtures/text_response_v7_openai.js +347 -0
- package/src/fixtures/text_response_v7_perplexity.js +764 -0
- package/src/generate.js +11 -11
- package/src/generate.spec.js +73 -14
- package/src/index.d.ts +169 -78
- package/src/prompt/loader.full.spec.js +21 -5
- package/src/prompt/loader.js +2 -18
- package/src/prompt/loader.spec.js +0 -30
- package/src/prompt/validations.js +37 -4
- package/src/prompt/validations.spec.js +65 -4
- package/src/utils/cost.full.spec.js +272 -0
- package/src/utils/cost.js +137 -0
- package/src/utils/cost.spec.js +290 -0
- package/src/utils/legacy_cost_attribute.js +75 -0
- package/src/utils/legacy_cost_attribute.spec.js +278 -0
- package/src/utils/models_pricing.js +68 -0
- package/src/{cost/fetch_models_pricing.spec.js → utils/models_pricing.spec.js} +3 -9
- package/src/utils/stream.js +2 -2
- package/src/utils/stream.spec.js +2 -2
- package/src/utils/usage.full.spec.js +215 -0
- package/src/utils/usage.js +121 -0
- package/src/utils/usage.spec.js +246 -0
- package/src/utils/wrap.js +42 -25
- package/src/utils/wrap.spec.js +162 -54
- package/src/validations.js +57 -19
- package/src/validations.spec.js +74 -7
- package/src/cost/fetch_models_pricing.js +0 -60
- package/src/cost/index.js +0 -58
- package/src/cost/index.spec.js +0 -350
- package/src/utils/__fixtures__/image_response.json +0 -38
- package/src/utils/__fixtures__/stream_response.json +0 -294
- package/src/utils/__fixtures__/text_response.json +0 -201
- /package/src/{cost/fixtures → fixtures}/models_api_light.json +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/llm",
|
|
3
|
-
"version": "0.11.1-next.
|
|
3
|
+
"version": "0.11.1-next.b4488bf.0",
|
|
4
4
|
"description": "Framework abstraction to interact with LLM models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -12,29 +12,30 @@
|
|
|
12
12
|
"./src"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"decimal.js": "10.6.0",
|
|
15
16
|
"entities": "8.0.0",
|
|
16
17
|
"gray-matter": "4.0.3",
|
|
17
18
|
"liquidjs": "10.27.2",
|
|
18
19
|
"undici": "8.9.0",
|
|
19
|
-
"@outputai/core": "0.11.1-next.
|
|
20
|
+
"@outputai/core": "0.11.1-next.b4488bf.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@ai-sdk/amazon-bedrock": "
|
|
23
|
-
"@ai-sdk/anthropic": "
|
|
24
|
-
"@ai-sdk/azure": "
|
|
25
|
-
"@ai-sdk/google-vertex": "
|
|
26
|
-
"@ai-sdk/openai": "
|
|
27
|
-
"@ai-sdk/perplexity": "
|
|
28
|
-
"ai": "
|
|
23
|
+
"@ai-sdk/amazon-bedrock": "5.0.57",
|
|
24
|
+
"@ai-sdk/anthropic": "4.0.39",
|
|
25
|
+
"@ai-sdk/azure": "4.0.43",
|
|
26
|
+
"@ai-sdk/google-vertex": "5.0.54",
|
|
27
|
+
"@ai-sdk/openai": "4.0.42",
|
|
28
|
+
"@ai-sdk/perplexity": "4.0.29",
|
|
29
|
+
"ai": "7.0.66"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
|
-
"@ai-sdk/amazon-bedrock": ">=
|
|
32
|
-
"@ai-sdk/anthropic": ">=
|
|
33
|
-
"@ai-sdk/azure": ">=
|
|
34
|
-
"@ai-sdk/google-vertex": ">=
|
|
35
|
-
"@ai-sdk/openai": ">=
|
|
36
|
-
"@ai-sdk/perplexity": ">=
|
|
37
|
-
"ai": ">=
|
|
32
|
+
"@ai-sdk/amazon-bedrock": ">=5 <6",
|
|
33
|
+
"@ai-sdk/anthropic": ">=4 <5",
|
|
34
|
+
"@ai-sdk/azure": ">=4 <5",
|
|
35
|
+
"@ai-sdk/google-vertex": ">=5 <6",
|
|
36
|
+
"@ai-sdk/openai": ">=4 <5",
|
|
37
|
+
"@ai-sdk/perplexity": ">=4 <5",
|
|
38
|
+
"ai": ">=7 <8"
|
|
38
39
|
},
|
|
39
40
|
"license": "Apache-2.0",
|
|
40
41
|
"publishConfig": {
|
package/src/agent.js
CHANGED
|
@@ -8,24 +8,23 @@ import { loadSkills } from './utils/skills.js';
|
|
|
8
8
|
import * as Validator from './validations.js';
|
|
9
9
|
import { Logger } from '@outputai/core';
|
|
10
10
|
|
|
11
|
-
export class Agent
|
|
11
|
+
export class Agent {
|
|
12
|
+
#agent;
|
|
12
13
|
#prompt;
|
|
13
14
|
#initialMessages;
|
|
14
15
|
#store;
|
|
15
16
|
|
|
16
17
|
constructor( args ) {
|
|
17
|
-
const { promptFile, promptDir, variables, tools, output, stopWhen, messageStore } = Validator.parseAgentArgs( args );
|
|
18
|
+
const { promptFile, promptObject, promptDir, variables, tools, output, stopWhen, messageStore } = Validator.parseAgentArgs( args );
|
|
18
19
|
|
|
19
|
-
const prompt = loadPrompt( promptFile, variables, promptDir );
|
|
20
|
+
const prompt = promptObject ?? loadPrompt( promptFile, variables, promptDir );
|
|
20
21
|
const skills = loadSkills( prompt );
|
|
21
22
|
|
|
22
|
-
const {
|
|
23
|
+
const { instructions, messages, ...aiOptions } = loadAiSdkTextOptions( { prompt, skills, tools, output, stopWhen } );
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
// per-message providerOptions); pass them as the agent's `instructions`.
|
|
26
|
-
super( {
|
|
25
|
+
this.#agent = new AIToolLoopAgent( {
|
|
27
26
|
...aiOptions,
|
|
28
|
-
...(
|
|
27
|
+
...( instructions.length > 0 ? { instructions } : {} )
|
|
29
28
|
} );
|
|
30
29
|
|
|
31
30
|
this.#prompt = prompt;
|
|
@@ -53,13 +52,13 @@ export class Agent extends AIToolLoopAgent {
|
|
|
53
52
|
name: 'Agent.generate',
|
|
54
53
|
prompt: this.#prompt,
|
|
55
54
|
fn: async () => {
|
|
56
|
-
const response = await
|
|
55
|
+
const response = await this.#agent.generate( {
|
|
57
56
|
messages: combinedMessages,
|
|
58
57
|
allowSystemInMessages: true,
|
|
59
58
|
...( abortSignal && { abortSignal } ),
|
|
60
59
|
...( toolChoice && { toolChoice } )
|
|
61
60
|
} );
|
|
62
|
-
await this.#storeMessages( messages.concat( response.
|
|
61
|
+
await this.#storeMessages( messages.concat( response.responseMessages ?? [] ) );
|
|
63
62
|
return response;
|
|
64
63
|
}
|
|
65
64
|
} );
|
|
@@ -77,13 +76,13 @@ export class Agent extends AIToolLoopAgent {
|
|
|
77
76
|
prompt: this.#prompt,
|
|
78
77
|
fn: async () => {
|
|
79
78
|
const state = { response: null };
|
|
80
|
-
const stream = await
|
|
79
|
+
const stream = await this.#agent.stream( {
|
|
81
80
|
messages: combinedMessages,
|
|
82
81
|
allowSystemInMessages: true,
|
|
83
82
|
...( onChunk && { onChunk } ),
|
|
84
83
|
...( abortSignal && { abortSignal } ),
|
|
85
84
|
...( toolChoice && { toolChoice } ),
|
|
86
|
-
|
|
85
|
+
onEnd: res => {
|
|
87
86
|
state.response = res;
|
|
88
87
|
},
|
|
89
88
|
onError: _ => {} // Suppress AI-SDK console printing
|
|
@@ -96,7 +95,7 @@ export class Agent extends AIToolLoopAgent {
|
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
state.response.output = await stream.output;
|
|
99
|
-
await this.#storeMessages( messages.concat( state.response.
|
|
98
|
+
await this.#storeMessages( messages.concat( state.response.responseMessages ?? [] ) );
|
|
100
99
|
|
|
101
100
|
return state.response;
|
|
102
101
|
}
|
|
@@ -104,28 +103,28 @@ export class Agent extends AIToolLoopAgent {
|
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
async stream( args ) {
|
|
107
|
-
const { messages, abortSignal, toolChoice, onChunk,
|
|
106
|
+
const { messages, abortSignal, toolChoice, onChunk, onEnd, onError } = Validator.parseAgentStreamArgs( args );
|
|
108
107
|
const combinedMessages = await this.#combineWithPreviousMessages( messages );
|
|
109
108
|
|
|
110
109
|
return wrapStream( {
|
|
111
110
|
name: 'Agent.stream',
|
|
112
111
|
prompt: this.#prompt,
|
|
113
|
-
fn: ( {
|
|
112
|
+
fn: ( { onEndHook, onErrorHook } ) => this.#agent.stream( {
|
|
114
113
|
messages: combinedMessages,
|
|
115
114
|
allowSystemInMessages: true,
|
|
116
115
|
...( onChunk && { onChunk } ),
|
|
117
116
|
...( abortSignal && { abortSignal } ),
|
|
118
117
|
...( toolChoice && { toolChoice } ),
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
onEnd: response =>
|
|
119
|
+
onEndHook( response, async parsedResponse => {
|
|
121
120
|
if ( response.finishReason !== 'error' ) {
|
|
122
|
-
await this.#storeMessages( messages.concat( response.
|
|
121
|
+
await this.#storeMessages( messages.concat( response.responseMessages ?? [] ) )
|
|
123
122
|
.catch( error => Logger.error( 'Agent.stream message store persistence failed', {
|
|
124
123
|
namespace: 'LLM',
|
|
125
124
|
error: error instanceof Error ? error.message : String( error )
|
|
126
125
|
} ) );
|
|
127
126
|
}
|
|
128
|
-
await
|
|
127
|
+
await onEnd?.( parsedResponse );
|
|
129
128
|
} ),
|
|
130
129
|
onError: event => onErrorHook( event, error => onError?.( { ...event, error } ) )
|
|
131
130
|
} )
|
package/src/agent.spec.js
CHANGED
|
@@ -38,7 +38,7 @@ const optionMocks = vi.hoisted( () => ( {
|
|
|
38
38
|
const wrapMocks = vi.hoisted( () => ( {
|
|
39
39
|
wrapGeneration: vi.fn(),
|
|
40
40
|
wrapStream: vi.fn(),
|
|
41
|
-
streamHooks: {
|
|
41
|
+
streamHooks: { onEndHook: vi.fn(), onErrorHook: vi.fn() }
|
|
42
42
|
} ) );
|
|
43
43
|
|
|
44
44
|
const streamMocks = vi.hoisted( () => ( {
|
|
@@ -113,7 +113,7 @@ const model = { id: 'MODEL' };
|
|
|
113
113
|
|
|
114
114
|
const textOptions = {
|
|
115
115
|
model,
|
|
116
|
-
|
|
116
|
+
instructions: [ { role: 'system', content: 'You are concise.' } ],
|
|
117
117
|
messages: [ { role: 'user', content: 'Initial user message' } ],
|
|
118
118
|
providerOptions: { test: true },
|
|
119
119
|
temperature: 0.3
|
|
@@ -124,7 +124,7 @@ const assistantMessage = { role: 'assistant', content: 'response' };
|
|
|
124
124
|
const aiResponse = {
|
|
125
125
|
text: 'response',
|
|
126
126
|
finishReason: 'stop',
|
|
127
|
-
|
|
127
|
+
responseMessages: [ assistantMessage ]
|
|
128
128
|
};
|
|
129
129
|
|
|
130
130
|
describe( 'Agent', () => {
|
|
@@ -145,7 +145,7 @@ describe( 'Agent', () => {
|
|
|
145
145
|
|
|
146
146
|
wrapMocks.wrapGeneration.mockReset().mockImplementation( async ( { fn } ) => fn() );
|
|
147
147
|
wrapMocks.streamHooks = {
|
|
148
|
-
|
|
148
|
+
onEndHook: vi.fn( async ( response, callback ) => callback?.( response ) ),
|
|
149
149
|
onErrorHook: vi.fn( ( event, callback ) => callback?.( event.error ) )
|
|
150
150
|
};
|
|
151
151
|
wrapMocks.wrapStream.mockReset().mockImplementation( ( { fn } ) => fn( wrapMocks.streamHooks ) );
|
|
@@ -213,6 +213,23 @@ describe( 'Agent', () => {
|
|
|
213
213
|
} );
|
|
214
214
|
} );
|
|
215
215
|
|
|
216
|
+
it( 'uses a prompt object without loading a prompt file', async () => {
|
|
217
|
+
validations.parseAgentArgs.mockReturnValueOnce( { promptObject: loadedPrompt } );
|
|
218
|
+
const { Agent } = await importSut();
|
|
219
|
+
|
|
220
|
+
new Agent( { prompt: loadedPrompt } );
|
|
221
|
+
|
|
222
|
+
expect( promptMocks.loadPrompt ).not.toHaveBeenCalled();
|
|
223
|
+
expect( skillMocks.loadSkills ).toHaveBeenCalledWith( loadedPrompt );
|
|
224
|
+
expect( optionMocks.loadAiSdkTextOptions ).toHaveBeenCalledWith( {
|
|
225
|
+
prompt: loadedPrompt,
|
|
226
|
+
skills: loadedSkills,
|
|
227
|
+
tools: undefined,
|
|
228
|
+
output: undefined,
|
|
229
|
+
stopWhen: undefined
|
|
230
|
+
} );
|
|
231
|
+
} );
|
|
232
|
+
|
|
216
233
|
it( 'passes option tools through to ToolLoopAgent', async () => {
|
|
217
234
|
const { Agent } = await importSut();
|
|
218
235
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
@@ -238,7 +255,7 @@ describe( 'Agent', () => {
|
|
|
238
255
|
};
|
|
239
256
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
240
257
|
model,
|
|
241
|
-
|
|
258
|
+
instructions: [ systemMessage ],
|
|
242
259
|
messages: [ { role: 'user', content: 'Hello' } ]
|
|
243
260
|
} );
|
|
244
261
|
|
|
@@ -255,7 +272,7 @@ describe( 'Agent', () => {
|
|
|
255
272
|
const { Agent } = await importSut();
|
|
256
273
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
257
274
|
model,
|
|
258
|
-
|
|
275
|
+
instructions: [],
|
|
259
276
|
messages: [ { role: 'user', content: 'Hello' } ]
|
|
260
277
|
} );
|
|
261
278
|
|
|
@@ -288,7 +305,7 @@ describe( 'Agent', () => {
|
|
|
288
305
|
const { Agent } = await importSut();
|
|
289
306
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
290
307
|
model,
|
|
291
|
-
|
|
308
|
+
instructions: [ { role: 'system', content: 'You are concise.' } ],
|
|
292
309
|
messages: [
|
|
293
310
|
{ role: 'user', content: 'Initial user message' },
|
|
294
311
|
{ role: 'assistant', content: 'Authored assistant block' }
|
|
@@ -376,7 +393,7 @@ describe( 'Agent', () => {
|
|
|
376
393
|
const response = { ...aiResponse };
|
|
377
394
|
aiMocks.superStream.mockImplementationOnce( options => {
|
|
378
395
|
options.onChunk( { chunk } );
|
|
379
|
-
options.
|
|
396
|
+
options.onEnd( response );
|
|
380
397
|
return stream;
|
|
381
398
|
} );
|
|
382
399
|
const { Agent } = await importSut();
|
|
@@ -409,7 +426,7 @@ describe( 'Agent', () => {
|
|
|
409
426
|
onChunk,
|
|
410
427
|
abortSignal,
|
|
411
428
|
toolChoice: 'required',
|
|
412
|
-
|
|
429
|
+
onEnd: expect.any( Function ),
|
|
413
430
|
onError: expect.any( Function )
|
|
414
431
|
} );
|
|
415
432
|
expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortSignal );
|
|
@@ -424,7 +441,7 @@ describe( 'Agent', () => {
|
|
|
424
441
|
it( 'omits onChunk when generateWithStreaming does not receive it', async () => {
|
|
425
442
|
const stream = { output: Promise.resolve( undefined ) };
|
|
426
443
|
aiMocks.superStream.mockImplementationOnce( options => {
|
|
427
|
-
options.
|
|
444
|
+
options.onEnd( { ...aiResponse } );
|
|
428
445
|
return stream;
|
|
429
446
|
} );
|
|
430
447
|
const { Agent } = await importSut();
|
|
@@ -453,14 +470,14 @@ describe( 'Agent', () => {
|
|
|
453
470
|
expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortController.signal );
|
|
454
471
|
} );
|
|
455
472
|
|
|
456
|
-
it( 'streams with initial, stored, and caller messages and stores on
|
|
473
|
+
it( 'streams with initial, stored, and caller messages and stores on end', async () => {
|
|
457
474
|
const store = {
|
|
458
475
|
getMessages: vi.fn( () => [
|
|
459
476
|
{ role: 'assistant', content: 'Stored reply' }
|
|
460
477
|
] ),
|
|
461
478
|
addMessages: vi.fn()
|
|
462
479
|
};
|
|
463
|
-
const
|
|
480
|
+
const onEnd = vi.fn();
|
|
464
481
|
const onError = vi.fn();
|
|
465
482
|
const onChunk = vi.fn();
|
|
466
483
|
const callerMessage = { role: 'user', content: 'New question' };
|
|
@@ -469,14 +486,14 @@ describe( 'Agent', () => {
|
|
|
469
486
|
|
|
470
487
|
const result = await agent.stream( {
|
|
471
488
|
messages: [ callerMessage ],
|
|
472
|
-
|
|
489
|
+
onEnd,
|
|
473
490
|
onError,
|
|
474
491
|
onChunk
|
|
475
492
|
} );
|
|
476
493
|
|
|
477
494
|
expect( validations.parseAgentStreamArgs ).toHaveBeenCalledWith( {
|
|
478
495
|
messages: [ callerMessage ],
|
|
479
|
-
|
|
496
|
+
onEnd,
|
|
480
497
|
onError,
|
|
481
498
|
onChunk
|
|
482
499
|
} );
|
|
@@ -493,40 +510,40 @@ describe( 'Agent', () => {
|
|
|
493
510
|
],
|
|
494
511
|
allowSystemInMessages: true,
|
|
495
512
|
onChunk,
|
|
496
|
-
|
|
513
|
+
onEnd: expect.any( Function ),
|
|
497
514
|
onError: expect.any( Function )
|
|
498
515
|
} );
|
|
499
516
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
500
|
-
await streamOptions.
|
|
501
|
-
expect( wrapMocks.streamHooks.
|
|
517
|
+
await streamOptions.onEnd( aiResponse );
|
|
518
|
+
expect( wrapMocks.streamHooks.onEndHook ).toHaveBeenCalledWith( aiResponse, expect.any( Function ) );
|
|
502
519
|
expect( store.addMessages ).toHaveBeenCalledWith( [
|
|
503
520
|
callerMessage,
|
|
504
521
|
assistantMessage
|
|
505
522
|
] );
|
|
506
|
-
expect(
|
|
523
|
+
expect( onEnd ).toHaveBeenCalledWith( aiResponse );
|
|
507
524
|
expect( result ).toEqual( { textStream: 'stream' } );
|
|
508
525
|
} );
|
|
509
526
|
|
|
510
|
-
it( 'logs stream persistence failures and still completes
|
|
527
|
+
it( 'logs stream persistence failures and still completes onEnd', async () => {
|
|
511
528
|
const persistenceError = new Error( 'Store unavailable' );
|
|
512
529
|
const store = {
|
|
513
530
|
getMessages: vi.fn( () => [] ),
|
|
514
531
|
addMessages: vi.fn().mockRejectedValue( persistenceError )
|
|
515
532
|
};
|
|
516
|
-
const
|
|
533
|
+
const onEnd = vi.fn();
|
|
517
534
|
const { Agent } = await importSut();
|
|
518
535
|
const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
|
|
519
536
|
|
|
520
|
-
const result = await agent.stream( {
|
|
537
|
+
const result = await agent.stream( { onEnd } );
|
|
521
538
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
522
539
|
|
|
523
|
-
await expect( streamOptions.
|
|
540
|
+
await expect( streamOptions.onEnd( aiResponse ) ).resolves.toBeUndefined();
|
|
524
541
|
|
|
525
542
|
expect( loggerMocks.error ).toHaveBeenCalledWith(
|
|
526
543
|
'Agent.stream message store persistence failed',
|
|
527
544
|
{ namespace: 'LLM', error: persistenceError.message }
|
|
528
545
|
);
|
|
529
|
-
expect(
|
|
546
|
+
expect( onEnd ).toHaveBeenCalledWith( aiResponse );
|
|
530
547
|
expect( result ).toEqual( { textStream: 'stream' } );
|
|
531
548
|
} );
|
|
532
549
|
|
|
@@ -535,16 +552,16 @@ describe( 'Agent', () => {
|
|
|
535
552
|
getMessages: vi.fn( () => [] ),
|
|
536
553
|
addMessages: vi.fn()
|
|
537
554
|
};
|
|
538
|
-
const
|
|
555
|
+
const onEnd = vi.fn();
|
|
539
556
|
const { Agent } = await importSut();
|
|
540
557
|
const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
|
|
541
558
|
|
|
542
|
-
await agent.stream( {
|
|
559
|
+
await agent.stream( { onEnd } );
|
|
543
560
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
544
|
-
await streamOptions.
|
|
561
|
+
await streamOptions.onEnd( { ...aiResponse, finishReason: 'error' } );
|
|
545
562
|
|
|
546
563
|
expect( store.addMessages ).not.toHaveBeenCalled();
|
|
547
|
-
expect(
|
|
564
|
+
expect( onEnd ).toHaveBeenCalledWith( { ...aiResponse, finishReason: 'error' } );
|
|
548
565
|
} );
|
|
549
566
|
|
|
550
567
|
it( 'omits onChunk when stream does not receive it', async () => {
|
package/src/ai_sdk_options.js
CHANGED
|
@@ -2,12 +2,36 @@ import { loadImageModel, loadTextModel } from './utils/models.js';
|
|
|
2
2
|
import { buildLoadSkillTool, loadPromptTools } from './utils/tools.js';
|
|
3
3
|
import { Role } from './consts.js';
|
|
4
4
|
import { FatalError } from '@outputai/core';
|
|
5
|
-
import {
|
|
5
|
+
import { isStepCount } from 'ai';
|
|
6
6
|
|
|
7
7
|
const buildSkillsMessageContent = skills =>
|
|
8
8
|
'Available skills (use load_skill to get full instructions):\n' +
|
|
9
9
|
skills.map( s => `- ${s.name}: ${s.description}` ).join( '\n' );
|
|
10
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
|
+
|
|
11
35
|
/**
|
|
12
36
|
* Build options for AI SDK text generation.
|
|
13
37
|
*
|
|
@@ -29,15 +53,15 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
29
53
|
throw new FatalError( `Prompt "${prompt.name}" has no chat-style messages. Add role-tagged blocks like <system> or <user>.` );
|
|
30
54
|
}
|
|
31
55
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
56
|
+
const instructions = prompt.messages.filter( m => m.role === Role.SYSTEM );
|
|
57
|
+
const messages = prompt.messages.filter( m => m.role !== Role.SYSTEM );
|
|
34
58
|
|
|
35
59
|
if ( skills.length > 0 ) {
|
|
36
60
|
const skillsMessageContent = buildSkillsMessageContent( skills );
|
|
37
|
-
if (
|
|
38
|
-
|
|
61
|
+
if ( instructions.length > 0 ) {
|
|
62
|
+
instructions[0] = { ...instructions[0], content: `${instructions[0].content}\n\n${skillsMessageContent}` };
|
|
39
63
|
} else {
|
|
40
|
-
|
|
64
|
+
instructions.push( { role: Role.SYSTEM, content: skillsMessageContent } );
|
|
41
65
|
}
|
|
42
66
|
}
|
|
43
67
|
|
|
@@ -45,14 +69,12 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
45
69
|
allowSystemInMessages: true,
|
|
46
70
|
maxRetries: 0,
|
|
47
71
|
model: loadTextModel( prompt ),
|
|
48
|
-
|
|
49
|
-
messages
|
|
50
|
-
providerOptions: prompt.config.providerOptions,
|
|
72
|
+
instructions,
|
|
73
|
+
messages,
|
|
51
74
|
...( output && { output } ),
|
|
52
75
|
...( abortSignal && { abortSignal } ),
|
|
53
76
|
...( stopWhen && { stopWhen } ),
|
|
54
|
-
...(
|
|
55
|
-
...( Number.isFinite( prompt.config.maxTokens ) && { maxOutputTokens: prompt.config.maxTokens } )
|
|
77
|
+
...select( textPromptConfigKeys, prompt.config )
|
|
56
78
|
};
|
|
57
79
|
|
|
58
80
|
// Tools parsing
|
|
@@ -65,7 +87,7 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
65
87
|
options.toolChoice = toolChoice;
|
|
66
88
|
}
|
|
67
89
|
if ( !options.stopWhen ) {
|
|
68
|
-
options.stopWhen =
|
|
90
|
+
options.stopWhen = isStepCount( prompt.config.maxSteps );
|
|
69
91
|
}
|
|
70
92
|
}
|
|
71
93
|
|
|
@@ -90,7 +112,7 @@ export const loadAiSdkImageOptions = ( { prompt, images, mask, abortSignal } ) =
|
|
|
90
112
|
if ( !prompt.instructions ) {
|
|
91
113
|
throw new FatalError( `Prompt "${prompt.name}" has no instructions. Image prompts must use plain instructions.` );
|
|
92
114
|
}
|
|
93
|
-
|
|
115
|
+
return {
|
|
94
116
|
maxRetries: 0,
|
|
95
117
|
model: loadImageModel( prompt ),
|
|
96
118
|
prompt: ( images || mask ) ? {
|
|
@@ -98,13 +120,7 @@ export const loadAiSdkImageOptions = ( { prompt, images, mask, abortSignal } ) =
|
|
|
98
120
|
...( images && { images } ),
|
|
99
121
|
...( mask && { mask } )
|
|
100
122
|
} : prompt.instructions,
|
|
101
|
-
|
|
102
|
-
...(
|
|
123
|
+
...( abortSignal && { abortSignal } ),
|
|
124
|
+
...select( imagePromptConfigKeys, prompt.config )
|
|
103
125
|
};
|
|
104
|
-
for ( const key of [ 'n', 'maxImagesPerCall', 'size', 'aspectRatio', 'seed' ] ) {
|
|
105
|
-
if ( prompt.config[key] !== undefined ) {
|
|
106
|
-
options[key] = prompt.config[key];
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return options;
|
|
110
126
|
};
|
|
@@ -16,7 +16,7 @@ vi.mock( './utils/tools.js', () => ( {
|
|
|
16
16
|
} ) );
|
|
17
17
|
|
|
18
18
|
vi.mock( 'ai', () => ( {
|
|
19
|
-
|
|
19
|
+
isStepCount: count => ( { type: 'step-count', count } )
|
|
20
20
|
} ) );
|
|
21
21
|
|
|
22
22
|
const importSut = async () => import( './ai_sdk_options.js' );
|
|
@@ -72,10 +72,10 @@ describe( 'ai_sdk_options', () => {
|
|
|
72
72
|
return loadAiSdkTextOptions( { skills: [], ...args } );
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
it( 'maps a loaded prompt to model,
|
|
75
|
+
it( 'maps a loaded prompt to model, instructions, messages, and generation config', async () => {
|
|
76
76
|
const prompt = makeTextPrompt( {
|
|
77
77
|
temperature: 0.3,
|
|
78
|
-
|
|
78
|
+
maxOutputTokens: 1000,
|
|
79
79
|
providerOptions: { anthropic: { effort: 'medium' } }
|
|
80
80
|
} );
|
|
81
81
|
|
|
@@ -87,7 +87,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
87
87
|
allowSystemInMessages: true,
|
|
88
88
|
maxRetries: 0,
|
|
89
89
|
model: 'MODEL',
|
|
90
|
-
|
|
90
|
+
instructions: [ { role: 'system', content: 'You are concise.' } ],
|
|
91
91
|
messages: [ { role: 'user', content: 'Hello' } ],
|
|
92
92
|
providerOptions: prompt.config.providerOptions,
|
|
93
93
|
temperature: 0.3,
|
|
@@ -95,6 +95,24 @@ describe( 'ai_sdk_options', () => {
|
|
|
95
95
|
} );
|
|
96
96
|
} );
|
|
97
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 );
|
|
114
|
+
} );
|
|
115
|
+
|
|
98
116
|
it( 'preserves temperature 0', async () => {
|
|
99
117
|
const result = await loadText( { prompt: makeTextPrompt( { temperature: 0 } ) } );
|
|
100
118
|
|
|
@@ -128,7 +146,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
128
146
|
|
|
129
147
|
const result = await loadText( { prompt } );
|
|
130
148
|
|
|
131
|
-
expect( result.
|
|
149
|
+
expect( result.instructions ).toEqual( [ {
|
|
132
150
|
role: 'system',
|
|
133
151
|
content: 'Static',
|
|
134
152
|
providerOptions
|
|
@@ -136,7 +154,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
136
154
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
137
155
|
} );
|
|
138
156
|
|
|
139
|
-
it( 'returns an empty
|
|
157
|
+
it( 'returns an empty instructions array when the prompt has no system block', async () => {
|
|
140
158
|
const prompt = {
|
|
141
159
|
name: 'no-system@v1',
|
|
142
160
|
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
@@ -146,11 +164,11 @@ describe( 'ai_sdk_options', () => {
|
|
|
146
164
|
|
|
147
165
|
const result = await loadText( { prompt } );
|
|
148
166
|
|
|
149
|
-
expect( result.
|
|
167
|
+
expect( result.instructions ).toEqual( [] );
|
|
150
168
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
151
169
|
} );
|
|
152
170
|
|
|
153
|
-
it( 'groups multiple system blocks into
|
|
171
|
+
it( 'groups multiple system blocks into instructions and keeps them out of messages', async () => {
|
|
154
172
|
const prompt = {
|
|
155
173
|
name: 'multi-system@v1',
|
|
156
174
|
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
@@ -164,7 +182,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
164
182
|
|
|
165
183
|
const result = await loadText( { prompt } );
|
|
166
184
|
|
|
167
|
-
expect( result.
|
|
185
|
+
expect( result.instructions ).toEqual( [
|
|
168
186
|
{ role: 'system', content: 'First' },
|
|
169
187
|
{ role: 'system', content: 'Second' }
|
|
170
188
|
] );
|
|
@@ -217,7 +235,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
217
235
|
} );
|
|
218
236
|
|
|
219
237
|
expect( buildLoadSkillToolImpl ).toHaveBeenCalledWith( [ writerSkill ] );
|
|
220
|
-
expect( result.
|
|
238
|
+
expect( result.instructions ).toEqual( [
|
|
221
239
|
{ role: 'system', content: `You are concise.\n\n${skillsCatalog}`, providerOptions }
|
|
222
240
|
] );
|
|
223
241
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
@@ -237,7 +255,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
237
255
|
|
|
238
256
|
const result = await loadText( { prompt, skills: [ writerSkill ] } );
|
|
239
257
|
|
|
240
|
-
expect( result.
|
|
258
|
+
expect( result.instructions ).toEqual( [ { role: 'system', content: skillsCatalog } ] );
|
|
241
259
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
242
260
|
} );
|
|
243
261
|
|
|
@@ -310,7 +328,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
310
328
|
aspectRatio: '1:1',
|
|
311
329
|
seed: 42,
|
|
312
330
|
temperature: 0.7,
|
|
313
|
-
|
|
331
|
+
maxOutputTokens: 1000,
|
|
314
332
|
providerOptions: { openai: { quality: 'high' } }
|
|
315
333
|
} );
|
|
316
334
|
|