@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.
Files changed (48) hide show
  1. package/package.json +17 -16
  2. package/src/agent.js +18 -19
  3. package/src/agent.spec.js +44 -27
  4. package/src/ai_sdk_options.js +37 -21
  5. package/src/ai_sdk_options.spec.js +30 -12
  6. package/src/fixtures/image_response_v7_google_vertex.js +72 -0
  7. package/src/fixtures/image_response_v7_openai.js +77 -0
  8. package/src/fixtures/models_api_v7_subset.json +259 -0
  9. package/src/fixtures/stream_response_v7_anthropic.js +389 -0
  10. package/src/fixtures/stream_response_v7_google_vertex.js +462 -0
  11. package/src/fixtures/stream_response_v7_openai.js +353 -0
  12. package/src/fixtures/stream_response_v7_perplexity.js +671 -0
  13. package/src/fixtures/text_response_v7_anthropic.js +359 -0
  14. package/src/fixtures/text_response_v7_anthropic_cache.js +719 -0
  15. package/src/fixtures/text_response_v7_google_vertex.js +435 -0
  16. package/src/fixtures/text_response_v7_openai.js +347 -0
  17. package/src/fixtures/text_response_v7_perplexity.js +764 -0
  18. package/src/generate.js +11 -11
  19. package/src/generate.spec.js +73 -14
  20. package/src/index.d.ts +169 -78
  21. package/src/prompt/loader.full.spec.js +21 -5
  22. package/src/prompt/loader.js +2 -18
  23. package/src/prompt/loader.spec.js +0 -30
  24. package/src/prompt/validations.js +37 -4
  25. package/src/prompt/validations.spec.js +65 -4
  26. package/src/utils/cost.full.spec.js +272 -0
  27. package/src/utils/cost.js +137 -0
  28. package/src/utils/cost.spec.js +290 -0
  29. package/src/utils/legacy_cost_attribute.js +75 -0
  30. package/src/utils/legacy_cost_attribute.spec.js +278 -0
  31. package/src/utils/models_pricing.js +68 -0
  32. package/src/{cost/fetch_models_pricing.spec.js → utils/models_pricing.spec.js} +3 -9
  33. package/src/utils/stream.js +2 -2
  34. package/src/utils/stream.spec.js +2 -2
  35. package/src/utils/usage.full.spec.js +215 -0
  36. package/src/utils/usage.js +121 -0
  37. package/src/utils/usage.spec.js +246 -0
  38. package/src/utils/wrap.js +42 -25
  39. package/src/utils/wrap.spec.js +162 -54
  40. package/src/validations.js +57 -19
  41. package/src/validations.spec.js +74 -7
  42. package/src/cost/fetch_models_pricing.js +0 -60
  43. package/src/cost/index.js +0 -58
  44. package/src/cost/index.spec.js +0 -350
  45. package/src/utils/__fixtures__/image_response.json +0 -38
  46. package/src/utils/__fixtures__/stream_response.json +0 -294
  47. package/src/utils/__fixtures__/text_response.json +0 -201
  48. /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.b3beef1.0",
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.b3beef1.0"
20
+ "@outputai/core": "0.11.1-next.b4488bf.0"
20
21
  },
21
22
  "devDependencies": {
22
- "@ai-sdk/amazon-bedrock": "4.0.138",
23
- "@ai-sdk/anthropic": "3.0.81",
24
- "@ai-sdk/azure": "3.0.68",
25
- "@ai-sdk/google-vertex": "4.0.140",
26
- "@ai-sdk/openai": "3.0.67",
27
- "@ai-sdk/perplexity": "3.0.33",
28
- "ai": "6.0.168"
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": ">=4 <5",
32
- "@ai-sdk/anthropic": ">=3 <4",
33
- "@ai-sdk/azure": ">=3 <4",
34
- "@ai-sdk/google-vertex": ">=4 <5",
35
- "@ai-sdk/openai": ">=3 <4",
36
- "@ai-sdk/perplexity": ">=3 <4",
37
- "ai": ">=6 <7"
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 extends AIToolLoopAgent {
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 { system, messages, ...aiOptions } = loadAiSdkTextOptions( { prompt, skills, tools, output, stopWhen } );
23
+ const { instructions, messages, ...aiOptions } = loadAiSdkTextOptions( { prompt, skills, tools, output, stopWhen } );
23
24
 
24
- // loadAiSdkTextOptions routes system blocks to the `system` slot (preserving
25
- // per-message providerOptions); pass them as the agent's `instructions`.
26
- super( {
25
+ this.#agent = new AIToolLoopAgent( {
27
26
  ...aiOptions,
28
- ...( system.length > 0 ? { instructions: system } : {} )
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 super.generate( {
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.response?.messages ?? [] ) );
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 super.stream( {
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
- onFinish: res => {
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.response?.messages ?? [] ) );
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, onFinish, onError } = Validator.parseAgentStreamArgs( args );
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: ( { onFinishHook, onErrorHook } ) => super.stream( {
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
- onFinish: response =>
120
- onFinishHook( response, async parsedResponse => {
118
+ onEnd: response =>
119
+ onEndHook( response, async parsedResponse => {
121
120
  if ( response.finishReason !== 'error' ) {
122
- await this.#storeMessages( messages.concat( response.response?.messages ?? [] ) )
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 onFinish?.( parsedResponse );
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: { onFinishHook: vi.fn(), onErrorHook: vi.fn() }
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
- system: [ { role: 'system', content: 'You are concise.' } ],
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
- response: { messages: [ assistantMessage ] }
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
- onFinishHook: vi.fn( async ( response, callback ) => callback?.( response ) ),
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
- system: [ systemMessage ],
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
- system: [],
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
- system: [ { role: 'system', content: 'You are concise.' } ],
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.onFinish( response );
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
- onFinish: expect.any( Function ),
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.onFinish( { ...aiResponse } );
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 finish', async () => {
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 onFinish = vi.fn();
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
- onFinish,
489
+ onEnd,
473
490
  onError,
474
491
  onChunk
475
492
  } );
476
493
 
477
494
  expect( validations.parseAgentStreamArgs ).toHaveBeenCalledWith( {
478
495
  messages: [ callerMessage ],
479
- onFinish,
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
- onFinish: expect.any( Function ),
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.onFinish( aiResponse );
501
- expect( wrapMocks.streamHooks.onFinishHook ).toHaveBeenCalledWith( aiResponse, expect.any( Function ) );
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( onFinish ).toHaveBeenCalledWith( aiResponse );
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 onFinish', async () => {
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 onFinish = vi.fn();
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( { onFinish } );
537
+ const result = await agent.stream( { onEnd } );
521
538
  const streamOptions = aiMocks.superStream.mock.calls[0][0];
522
539
 
523
- await expect( streamOptions.onFinish( aiResponse ) ).resolves.toBeUndefined();
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( onFinish ).toHaveBeenCalledWith( aiResponse );
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 onFinish = vi.fn();
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( { onFinish } );
559
+ await agent.stream( { onEnd } );
543
560
  const streamOptions = aiMocks.superStream.mock.calls[0][0];
544
- await streamOptions.onFinish( { ...aiResponse, finishReason: 'error' } );
561
+ await streamOptions.onEnd( { ...aiResponse, finishReason: 'error' } );
545
562
 
546
563
  expect( store.addMessages ).not.toHaveBeenCalled();
547
- expect( onFinish ).toHaveBeenCalledWith( { ...aiResponse, finishReason: 'error' } );
564
+ expect( onEnd ).toHaveBeenCalledWith( { ...aiResponse, finishReason: 'error' } );
548
565
  } );
549
566
 
550
567
  it( 'omits onChunk when stream does not receive it', async () => {
@@ -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 { stepCountIs } from 'ai';
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 systemMessages = prompt.messages.filter( m => m.role === Role.SYSTEM );
33
- const allMessages = prompt.messages.filter( m => m.role !== Role.SYSTEM );
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 ( systemMessages.length > 0 ) {
38
- systemMessages[0] = { ...systemMessages[0], content: `${systemMessages[0].content}\n\n${skillsMessageContent}` };
61
+ if ( instructions.length > 0 ) {
62
+ instructions[0] = { ...instructions[0], content: `${instructions[0].content}\n\n${skillsMessageContent}` };
39
63
  } else {
40
- systemMessages.push( { role: Role.SYSTEM, content: skillsMessageContent } );
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
- system: systemMessages,
49
- messages: allMessages,
50
- providerOptions: prompt.config.providerOptions,
72
+ instructions,
73
+ messages,
51
74
  ...( output && { output } ),
52
75
  ...( abortSignal && { abortSignal } ),
53
76
  ...( stopWhen && { stopWhen } ),
54
- ...( Number.isFinite( prompt.config.temperature ) && { temperature: prompt.config.temperature } ),
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 = stepCountIs( prompt.config.maxSteps );
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
- const options = {
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
- providerOptions: prompt.config.providerOptions,
102
- ...( abortSignal && { abortSignal } )
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
- stepCountIs: count => ( { type: 'step-count', count } )
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, system, messages, and generation config', async () => {
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
- maxTokens: 1000,
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
- system: [ { role: 'system', content: 'You are concise.' } ],
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.system ).toEqual( [ {
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 system array when the prompt has no system block', async () => {
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.system ).toEqual( [] );
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 system and keeps them out of messages', async () => {
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.system ).toEqual( [
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.system ).toEqual( [
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.system ).toEqual( [ { role: 'system', content: skillsCatalog } ] );
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
- maxTokens: 1000,
331
+ maxOutputTokens: 1000,
314
332
  providerOptions: { openai: { quality: 'high' } }
315
333
  } );
316
334