@outputai/llm 0.11.1-next.a5ded18.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 +16 -17
- package/src/agent.spec.js +27 -27
- package/src/ai_sdk_options.js +9 -9
- package/src/ai_sdk_options.spec.js +10 -10
- 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 +4 -4
- package/src/generate.spec.js +15 -15
- package/src/index.d.ts +113 -59
- 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 +2 -2
- package/src/validations.spec.js +7 -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,7 +8,8 @@ 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;
|
|
@@ -19,13 +20,11 @@ export class Agent extends AIToolLoopAgent {
|
|
|
19
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 ) );
|
|
@@ -255,7 +255,7 @@ describe( 'Agent', () => {
|
|
|
255
255
|
};
|
|
256
256
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
257
257
|
model,
|
|
258
|
-
|
|
258
|
+
instructions: [ systemMessage ],
|
|
259
259
|
messages: [ { role: 'user', content: 'Hello' } ]
|
|
260
260
|
} );
|
|
261
261
|
|
|
@@ -272,7 +272,7 @@ describe( 'Agent', () => {
|
|
|
272
272
|
const { Agent } = await importSut();
|
|
273
273
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
274
274
|
model,
|
|
275
|
-
|
|
275
|
+
instructions: [],
|
|
276
276
|
messages: [ { role: 'user', content: 'Hello' } ]
|
|
277
277
|
} );
|
|
278
278
|
|
|
@@ -305,7 +305,7 @@ describe( 'Agent', () => {
|
|
|
305
305
|
const { Agent } = await importSut();
|
|
306
306
|
optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
|
|
307
307
|
model,
|
|
308
|
-
|
|
308
|
+
instructions: [ { role: 'system', content: 'You are concise.' } ],
|
|
309
309
|
messages: [
|
|
310
310
|
{ role: 'user', content: 'Initial user message' },
|
|
311
311
|
{ role: 'assistant', content: 'Authored assistant block' }
|
|
@@ -393,7 +393,7 @@ describe( 'Agent', () => {
|
|
|
393
393
|
const response = { ...aiResponse };
|
|
394
394
|
aiMocks.superStream.mockImplementationOnce( options => {
|
|
395
395
|
options.onChunk( { chunk } );
|
|
396
|
-
options.
|
|
396
|
+
options.onEnd( response );
|
|
397
397
|
return stream;
|
|
398
398
|
} );
|
|
399
399
|
const { Agent } = await importSut();
|
|
@@ -426,7 +426,7 @@ describe( 'Agent', () => {
|
|
|
426
426
|
onChunk,
|
|
427
427
|
abortSignal,
|
|
428
428
|
toolChoice: 'required',
|
|
429
|
-
|
|
429
|
+
onEnd: expect.any( Function ),
|
|
430
430
|
onError: expect.any( Function )
|
|
431
431
|
} );
|
|
432
432
|
expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortSignal );
|
|
@@ -441,7 +441,7 @@ describe( 'Agent', () => {
|
|
|
441
441
|
it( 'omits onChunk when generateWithStreaming does not receive it', async () => {
|
|
442
442
|
const stream = { output: Promise.resolve( undefined ) };
|
|
443
443
|
aiMocks.superStream.mockImplementationOnce( options => {
|
|
444
|
-
options.
|
|
444
|
+
options.onEnd( { ...aiResponse } );
|
|
445
445
|
return stream;
|
|
446
446
|
} );
|
|
447
447
|
const { Agent } = await importSut();
|
|
@@ -470,14 +470,14 @@ describe( 'Agent', () => {
|
|
|
470
470
|
expect( streamMocks.drainStream ).toHaveBeenCalledWith( stream, abortController.signal );
|
|
471
471
|
} );
|
|
472
472
|
|
|
473
|
-
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 () => {
|
|
474
474
|
const store = {
|
|
475
475
|
getMessages: vi.fn( () => [
|
|
476
476
|
{ role: 'assistant', content: 'Stored reply' }
|
|
477
477
|
] ),
|
|
478
478
|
addMessages: vi.fn()
|
|
479
479
|
};
|
|
480
|
-
const
|
|
480
|
+
const onEnd = vi.fn();
|
|
481
481
|
const onError = vi.fn();
|
|
482
482
|
const onChunk = vi.fn();
|
|
483
483
|
const callerMessage = { role: 'user', content: 'New question' };
|
|
@@ -486,14 +486,14 @@ describe( 'Agent', () => {
|
|
|
486
486
|
|
|
487
487
|
const result = await agent.stream( {
|
|
488
488
|
messages: [ callerMessage ],
|
|
489
|
-
|
|
489
|
+
onEnd,
|
|
490
490
|
onError,
|
|
491
491
|
onChunk
|
|
492
492
|
} );
|
|
493
493
|
|
|
494
494
|
expect( validations.parseAgentStreamArgs ).toHaveBeenCalledWith( {
|
|
495
495
|
messages: [ callerMessage ],
|
|
496
|
-
|
|
496
|
+
onEnd,
|
|
497
497
|
onError,
|
|
498
498
|
onChunk
|
|
499
499
|
} );
|
|
@@ -510,40 +510,40 @@ describe( 'Agent', () => {
|
|
|
510
510
|
],
|
|
511
511
|
allowSystemInMessages: true,
|
|
512
512
|
onChunk,
|
|
513
|
-
|
|
513
|
+
onEnd: expect.any( Function ),
|
|
514
514
|
onError: expect.any( Function )
|
|
515
515
|
} );
|
|
516
516
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
517
|
-
await streamOptions.
|
|
518
|
-
expect( wrapMocks.streamHooks.
|
|
517
|
+
await streamOptions.onEnd( aiResponse );
|
|
518
|
+
expect( wrapMocks.streamHooks.onEndHook ).toHaveBeenCalledWith( aiResponse, expect.any( Function ) );
|
|
519
519
|
expect( store.addMessages ).toHaveBeenCalledWith( [
|
|
520
520
|
callerMessage,
|
|
521
521
|
assistantMessage
|
|
522
522
|
] );
|
|
523
|
-
expect(
|
|
523
|
+
expect( onEnd ).toHaveBeenCalledWith( aiResponse );
|
|
524
524
|
expect( result ).toEqual( { textStream: 'stream' } );
|
|
525
525
|
} );
|
|
526
526
|
|
|
527
|
-
it( 'logs stream persistence failures and still completes
|
|
527
|
+
it( 'logs stream persistence failures and still completes onEnd', async () => {
|
|
528
528
|
const persistenceError = new Error( 'Store unavailable' );
|
|
529
529
|
const store = {
|
|
530
530
|
getMessages: vi.fn( () => [] ),
|
|
531
531
|
addMessages: vi.fn().mockRejectedValue( persistenceError )
|
|
532
532
|
};
|
|
533
|
-
const
|
|
533
|
+
const onEnd = vi.fn();
|
|
534
534
|
const { Agent } = await importSut();
|
|
535
535
|
const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
|
|
536
536
|
|
|
537
|
-
const result = await agent.stream( {
|
|
537
|
+
const result = await agent.stream( { onEnd } );
|
|
538
538
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
539
539
|
|
|
540
|
-
await expect( streamOptions.
|
|
540
|
+
await expect( streamOptions.onEnd( aiResponse ) ).resolves.toBeUndefined();
|
|
541
541
|
|
|
542
542
|
expect( loggerMocks.error ).toHaveBeenCalledWith(
|
|
543
543
|
'Agent.stream message store persistence failed',
|
|
544
544
|
{ namespace: 'LLM', error: persistenceError.message }
|
|
545
545
|
);
|
|
546
|
-
expect(
|
|
546
|
+
expect( onEnd ).toHaveBeenCalledWith( aiResponse );
|
|
547
547
|
expect( result ).toEqual( { textStream: 'stream' } );
|
|
548
548
|
} );
|
|
549
549
|
|
|
@@ -552,16 +552,16 @@ describe( 'Agent', () => {
|
|
|
552
552
|
getMessages: vi.fn( () => [] ),
|
|
553
553
|
addMessages: vi.fn()
|
|
554
554
|
};
|
|
555
|
-
const
|
|
555
|
+
const onEnd = vi.fn();
|
|
556
556
|
const { Agent } = await importSut();
|
|
557
557
|
const agent = new Agent( { prompt: 'test@v1', messageStore: store } );
|
|
558
558
|
|
|
559
|
-
await agent.stream( {
|
|
559
|
+
await agent.stream( { onEnd } );
|
|
560
560
|
const streamOptions = aiMocks.superStream.mock.calls[0][0];
|
|
561
|
-
await streamOptions.
|
|
561
|
+
await streamOptions.onEnd( { ...aiResponse, finishReason: 'error' } );
|
|
562
562
|
|
|
563
563
|
expect( store.addMessages ).not.toHaveBeenCalled();
|
|
564
|
-
expect(
|
|
564
|
+
expect( onEnd ).toHaveBeenCalledWith( { ...aiResponse, finishReason: 'error' } );
|
|
565
565
|
} );
|
|
566
566
|
|
|
567
567
|
it( 'omits onChunk when stream does not receive it', async () => {
|
package/src/ai_sdk_options.js
CHANGED
|
@@ -2,7 +2,7 @@ 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' +
|
|
@@ -53,15 +53,15 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
53
53
|
throw new FatalError( `Prompt "${prompt.name}" has no chat-style messages. Add role-tagged blocks like <system> or <user>.` );
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const instructions = prompt.messages.filter( m => m.role === Role.SYSTEM );
|
|
57
|
+
const messages = prompt.messages.filter( m => m.role !== Role.SYSTEM );
|
|
58
58
|
|
|
59
59
|
if ( skills.length > 0 ) {
|
|
60
60
|
const skillsMessageContent = buildSkillsMessageContent( skills );
|
|
61
|
-
if (
|
|
62
|
-
|
|
61
|
+
if ( instructions.length > 0 ) {
|
|
62
|
+
instructions[0] = { ...instructions[0], content: `${instructions[0].content}\n\n${skillsMessageContent}` };
|
|
63
63
|
} else {
|
|
64
|
-
|
|
64
|
+
instructions.push( { role: Role.SYSTEM, content: skillsMessageContent } );
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -69,8 +69,8 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
69
69
|
allowSystemInMessages: true,
|
|
70
70
|
maxRetries: 0,
|
|
71
71
|
model: loadTextModel( prompt ),
|
|
72
|
-
|
|
73
|
-
messages
|
|
72
|
+
instructions,
|
|
73
|
+
messages,
|
|
74
74
|
...( output && { output } ),
|
|
75
75
|
...( abortSignal && { abortSignal } ),
|
|
76
76
|
...( stopWhen && { stopWhen } ),
|
|
@@ -87,7 +87,7 @@ export const loadAiSdkTextOptions = ( { prompt, tools, skills, stopWhen, output,
|
|
|
87
87
|
options.toolChoice = toolChoice;
|
|
88
88
|
}
|
|
89
89
|
if ( !options.stopWhen ) {
|
|
90
|
-
options.stopWhen =
|
|
90
|
+
options.stopWhen = isStepCount( prompt.config.maxSteps );
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -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,7 +72,7 @@ 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,
|
|
@@ -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,
|
|
@@ -146,7 +146,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
146
146
|
|
|
147
147
|
const result = await loadText( { prompt } );
|
|
148
148
|
|
|
149
|
-
expect( result.
|
|
149
|
+
expect( result.instructions ).toEqual( [ {
|
|
150
150
|
role: 'system',
|
|
151
151
|
content: 'Static',
|
|
152
152
|
providerOptions
|
|
@@ -154,7 +154,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
154
154
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
155
155
|
} );
|
|
156
156
|
|
|
157
|
-
it( 'returns an empty
|
|
157
|
+
it( 'returns an empty instructions array when the prompt has no system block', async () => {
|
|
158
158
|
const prompt = {
|
|
159
159
|
name: 'no-system@v1',
|
|
160
160
|
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
@@ -164,11 +164,11 @@ describe( 'ai_sdk_options', () => {
|
|
|
164
164
|
|
|
165
165
|
const result = await loadText( { prompt } );
|
|
166
166
|
|
|
167
|
-
expect( result.
|
|
167
|
+
expect( result.instructions ).toEqual( [] );
|
|
168
168
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
169
169
|
} );
|
|
170
170
|
|
|
171
|
-
it( 'groups multiple system blocks into
|
|
171
|
+
it( 'groups multiple system blocks into instructions and keeps them out of messages', async () => {
|
|
172
172
|
const prompt = {
|
|
173
173
|
name: 'multi-system@v1',
|
|
174
174
|
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
@@ -182,7 +182,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
182
182
|
|
|
183
183
|
const result = await loadText( { prompt } );
|
|
184
184
|
|
|
185
|
-
expect( result.
|
|
185
|
+
expect( result.instructions ).toEqual( [
|
|
186
186
|
{ role: 'system', content: 'First' },
|
|
187
187
|
{ role: 'system', content: 'Second' }
|
|
188
188
|
] );
|
|
@@ -235,7 +235,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
235
235
|
} );
|
|
236
236
|
|
|
237
237
|
expect( buildLoadSkillToolImpl ).toHaveBeenCalledWith( [ writerSkill ] );
|
|
238
|
-
expect( result.
|
|
238
|
+
expect( result.instructions ).toEqual( [
|
|
239
239
|
{ role: 'system', content: `You are concise.\n\n${skillsCatalog}`, providerOptions }
|
|
240
240
|
] );
|
|
241
241
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
@@ -255,7 +255,7 @@ describe( 'ai_sdk_options', () => {
|
|
|
255
255
|
|
|
256
256
|
const result = await loadText( { prompt, skills: [ writerSkill ] } );
|
|
257
257
|
|
|
258
|
-
expect( result.
|
|
258
|
+
expect( result.instructions ).toEqual( [ { role: 'system', content: skillsCatalog } ] );
|
|
259
259
|
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
260
260
|
} );
|
|
261
261
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAfUlEQVR4Xu3VQREAIAwEMWqnD/y7AyGblcDQy+zed8KNB/ADnIANCG+gEaQABShAAQpgMBwFKEABClAgjAAFKEABClCAAhgMRwEKUIACFAgjQAEKUIACFKAABsNRgAIUoAAFwghQgAIUoAAFKIDBcBSgAAUoQIEwAhTIK/ABN/RggaSa6FYAAAAASUVORK5CYII=';
|
|
2
|
+
const bytes = [
|
|
3
|
+
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 64, 0, 0, 0, 64,
|
|
4
|
+
8, 6, 0, 0, 0, 170, 105, 113, 222, 0, 0, 0, 125, 73, 68, 65, 84, 120, 94, 237, 213, 65, 17,
|
|
5
|
+
0, 32, 12, 4, 49, 106, 167, 15, 252, 187, 3, 33, 155, 149, 192, 208, 203, 236, 222, 119, 194,
|
|
6
|
+
141, 7, 240, 3, 156, 128, 13, 8, 111, 160, 17, 164, 0, 5, 40, 64, 1, 10, 96, 48, 28, 5, 40,
|
|
7
|
+
64, 1, 10, 80, 32, 140, 0, 5, 40, 64, 1, 10, 80, 128, 2, 24, 12, 71, 1, 10, 80, 128, 2, 20,
|
|
8
|
+
8, 35, 64, 1, 10, 80, 128, 2, 20, 160, 0, 6, 195, 81, 128, 2, 20, 160, 0, 5, 194, 8, 80,
|
|
9
|
+
128, 2, 20, 160, 0, 5, 40, 128, 193, 112, 20, 160, 0, 5, 40, 64, 129, 48, 2, 20, 200, 43,
|
|
10
|
+
240, 1, 55, 244, 96, 129, 164, 154, 232, 86, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
images: [
|
|
15
|
+
{
|
|
16
|
+
base64Data: base64,
|
|
17
|
+
mediaType: 'image/png',
|
|
18
|
+
// getters
|
|
19
|
+
base64,
|
|
20
|
+
uint8Array: new Uint8Array( bytes )
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
warnings: [
|
|
24
|
+
{
|
|
25
|
+
type: 'unsupported',
|
|
26
|
+
feature: 'size',
|
|
27
|
+
details: 'This model does not support the `size` option. Use `aspectRatio` instead.'
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
responses: [
|
|
31
|
+
{
|
|
32
|
+
timestamp: '2026-08-25T00:00:00.000Z',
|
|
33
|
+
modelId: 'gemini-2.5-flash-image',
|
|
34
|
+
headers: {
|
|
35
|
+
'alt-svc': 'h3=":443"; ma=2592000,h3-29=":443"; ma=2592000',
|
|
36
|
+
'content-encoding': 'gzip',
|
|
37
|
+
'content-type': 'application/json; charset=UTF-8',
|
|
38
|
+
date: 'Tue, 25 Aug 2026 00:00:00 GMT',
|
|
39
|
+
server: 'scaffolding on HTTPServer2',
|
|
40
|
+
'transfer-encoding': 'chunked',
|
|
41
|
+
vary: 'Origin, X-Origin, Referer',
|
|
42
|
+
'x-content-type-options': 'nosniff',
|
|
43
|
+
'x-frame-options': 'SAMEORIGIN',
|
|
44
|
+
'x-xss-protection': '0'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
providerMetadata: {
|
|
49
|
+
googleVertex: {
|
|
50
|
+
images: [
|
|
51
|
+
{}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
vertex: {
|
|
55
|
+
images: [
|
|
56
|
+
{}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
usage: {
|
|
61
|
+
inputTokens: 157,
|
|
62
|
+
outputTokens: 1290,
|
|
63
|
+
totalTokens: 1447
|
|
64
|
+
},
|
|
65
|
+
image: {
|
|
66
|
+
base64Data: base64,
|
|
67
|
+
mediaType: 'image/png',
|
|
68
|
+
// getters
|
|
69
|
+
base64,
|
|
70
|
+
uint8Array: new Uint8Array( bytes )
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const base64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAfUlEQVR4Xu3VQREAIAwEMWqnD/y7AyGblcDQy+zed8KNB/ADnIANCG+gEaQABShAAQpgMBwFKEABClAgjAAFKEABClCAAhgMRwEKUIACFAgjQAEKUIACFKAABsNRgAIUoAAFwghQgAIUoAAFKIDBcBSgAAUoQIEwAhTIK/ABN/RggaSa6FYAAAAASUVORK5CYII=';
|
|
2
|
+
const bytes = [
|
|
3
|
+
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 64, 0, 0, 0, 64,
|
|
4
|
+
8, 6, 0, 0, 0, 170, 105, 113, 222, 0, 0, 0, 125, 73, 68, 65, 84, 120, 94, 237, 213, 65, 17,
|
|
5
|
+
0, 32, 12, 4, 49, 106, 167, 15, 252, 187, 3, 33, 155, 149, 192, 208, 203, 236, 222, 119, 194,
|
|
6
|
+
141, 7, 240, 3, 156, 128, 13, 8, 111, 160, 17, 164, 0, 5, 40, 64, 1, 10, 96, 48, 28, 5, 40,
|
|
7
|
+
64, 1, 10, 80, 32, 140, 0, 5, 40, 64, 1, 10, 80, 128, 2, 24, 12, 71, 1, 10, 80, 128, 2, 20,
|
|
8
|
+
8, 35, 64, 1, 10, 80, 128, 2, 20, 160, 0, 6, 195, 81, 128, 2, 20, 160, 0, 5, 194, 8, 80,
|
|
9
|
+
128, 2, 20, 160, 0, 5, 40, 128, 193, 112, 20, 160, 0, 5, 40, 64, 129, 48, 2, 20, 200, 43,
|
|
10
|
+
240, 1, 55, 244, 96, 129, 164, 154, 232, 86, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
images: [
|
|
15
|
+
{
|
|
16
|
+
base64Data: base64,
|
|
17
|
+
mediaType: 'image/png',
|
|
18
|
+
// getters
|
|
19
|
+
base64,
|
|
20
|
+
uint8Array: new Uint8Array( bytes )
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
warnings: [],
|
|
24
|
+
responses: [
|
|
25
|
+
{
|
|
26
|
+
timestamp: '2026-08-25T10:46:40.310Z',
|
|
27
|
+
modelId: 'gpt-image-1',
|
|
28
|
+
headers: {
|
|
29
|
+
'access-control-expose-headers': 'X-Request-ID, CF-Ray',
|
|
30
|
+
'alt-svc': 'h3=":443"; ma=86400',
|
|
31
|
+
'cf-cache-status': 'DYNAMIC',
|
|
32
|
+
'cf-ray': 'cf-id',
|
|
33
|
+
connection: 'keep-alive',
|
|
34
|
+
'content-encoding': 'br',
|
|
35
|
+
'content-type': 'application/json',
|
|
36
|
+
date: 'Tue, 25 Aug 2026 10:47:21 GMT',
|
|
37
|
+
'openai-organization': 'org-id',
|
|
38
|
+
'openai-processing-ms': '41337',
|
|
39
|
+
'openai-project': 'proj-id',
|
|
40
|
+
'openai-version': '2020-10-01',
|
|
41
|
+
server: 'cloudflare',
|
|
42
|
+
'set-cookie': '__cf_bm=token; HttpOnly; SameSite=None; Secure; Path=/; Domain=api.openai.com; Expires=Tue, 25 Aug 2026 00:00:00 GMT',
|
|
43
|
+
'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
|
|
44
|
+
'transfer-encoding': 'chunked',
|
|
45
|
+
'x-content-type-options': 'nosniff',
|
|
46
|
+
'x-request-id': 'req_id'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
providerMetadata: {
|
|
51
|
+
openai: {
|
|
52
|
+
images: [
|
|
53
|
+
{
|
|
54
|
+
created: 1787610084,
|
|
55
|
+
size: '1024x1024',
|
|
56
|
+
quality: 'high',
|
|
57
|
+
background: 'opaque',
|
|
58
|
+
outputFormat: 'png',
|
|
59
|
+
imageTokens: 0,
|
|
60
|
+
textTokens: 151
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
usage: {
|
|
66
|
+
inputTokens: 151,
|
|
67
|
+
outputTokens: 4160,
|
|
68
|
+
totalTokens: 4311
|
|
69
|
+
},
|
|
70
|
+
image: {
|
|
71
|
+
base64Data: base64,
|
|
72
|
+
mediaType: 'image/png',
|
|
73
|
+
// getters
|
|
74
|
+
base64,
|
|
75
|
+
uint8Array: new Uint8Array( bytes )
|
|
76
|
+
}
|
|
77
|
+
};
|