@outputai/llm 0.6.1-dev.daae905.0 → 0.6.1-next.2cc4685.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 +15 -9
- package/src/agent.spec.js +295 -214
- package/src/ai_model.js +79 -36
- package/src/ai_model.spec.js +31 -13
- package/src/ai_sdk.js +55 -79
- package/src/ai_sdk.spec.js +464 -611
- package/src/ai_sdk_options.js +61 -0
- package/src/ai_sdk_options.spec.js +164 -0
- package/src/cost/index.js +1 -1
- package/src/index.d.ts +230 -175
- package/src/index.js +2 -2
- package/src/prompt/escape.js +65 -0
- package/src/prompt/escape.spec.js +159 -0
- package/src/{load_content.js → prompt/load_content.js} +1 -22
- package/src/{load_content.spec.js → prompt/load_content.spec.js} +6 -6
- package/src/prompt/loader.js +49 -0
- package/src/prompt/loader.spec.js +274 -0
- package/src/{prompt_loader_validation.spec.js → prompt/loader_validation.spec.js} +40 -7
- package/src/prompt/parser.js +19 -0
- package/src/{parser.spec.js → prompt/parser.spec.js} +74 -29
- package/src/prompt/prepare_text.js +27 -0
- package/src/prompt/prepare_text.spec.js +141 -0
- package/src/{skill.js → prompt/skill.js} +19 -0
- package/src/prompt/skill.spec.js +172 -0
- package/src/{prompt_validations.js → prompt/validations.js} +32 -6
- package/src/{prompt_validations.spec.js → prompt/validations.spec.js} +189 -1
- package/src/utils/__fixtures__/image_response.json +38 -0
- package/src/utils/__fixtures__/stream_response.json +294 -0
- package/src/utils/__fixtures__/text_response.json +201 -0
- package/src/utils/error_handler.js +65 -0
- package/src/utils/error_handler.spec.js +195 -0
- package/src/utils/image.js +10 -0
- package/src/utils/image.spec.js +20 -0
- package/src/utils/response_wrappers.js +46 -19
- package/src/utils/response_wrappers.spec.js +130 -70
- package/src/utils/source_extraction.js +17 -27
- package/src/utils/trace.js +2 -3
- package/src/utils/trace.spec.js +9 -13
- package/src/validations.js +54 -2
- package/src/validations.spec.js +166 -0
- package/src/parser.js +0 -28
- package/src/prompt_loader.js +0 -80
- package/src/prompt_loader.spec.js +0 -358
- package/src/skill.d.ts +0 -49
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { loadImageModel, loadTextModel, loadTools } from './ai_model.js';
|
|
2
|
+
import { FatalError } from '@outputai/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Convert a loaded prompt into AI SDK text generation options.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} prompt - Loaded prompt object
|
|
8
|
+
* @returns {object} Options for AI SDK text calls
|
|
9
|
+
*/
|
|
10
|
+
export const loadAiSdkTextOptions = prompt => {
|
|
11
|
+
if ( prompt.messages.length === 0 ) {
|
|
12
|
+
throw new FatalError( `Prompt "${prompt.name}" has no chat-style messages. Add role-tagged blocks like <system> or <user>.` );
|
|
13
|
+
}
|
|
14
|
+
const options = {
|
|
15
|
+
model: loadTextModel( prompt ),
|
|
16
|
+
messages: prompt.messages,
|
|
17
|
+
providerOptions: prompt.config.providerOptions
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if ( Number.isFinite( prompt.config.temperature ) ) {
|
|
21
|
+
options.temperature = prompt.config.temperature;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if ( prompt.config.maxTokens ) {
|
|
25
|
+
options.maxOutputTokens = prompt.config.maxTokens;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tools = loadTools( prompt );
|
|
29
|
+
if ( tools ) {
|
|
30
|
+
options.tools = tools;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return options;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convert a loaded prompt into AI SDK image generation options.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} prompt - Loaded prompt object
|
|
40
|
+
* @returns {object} Options for AI SDK image calls
|
|
41
|
+
*/
|
|
42
|
+
export const loadAiSdkImageOptions = ( { prompt, images, mask } ) => {
|
|
43
|
+
if ( !prompt.instructions ) {
|
|
44
|
+
throw new FatalError( `Prompt "${prompt.name}" has no instructions. Image prompts must use plain instructions.` );
|
|
45
|
+
}
|
|
46
|
+
const options = {
|
|
47
|
+
model: loadImageModel( prompt ),
|
|
48
|
+
prompt: ( images || mask ) ? {
|
|
49
|
+
text: prompt.instructions,
|
|
50
|
+
...( images && { images } ),
|
|
51
|
+
...( mask && { mask } )
|
|
52
|
+
} : prompt.instructions,
|
|
53
|
+
providerOptions: prompt.config.providerOptions
|
|
54
|
+
};
|
|
55
|
+
for ( const key of [ 'n', 'maxImagesPerCall', 'size', 'aspectRatio', 'seed' ] ) {
|
|
56
|
+
if ( prompt.config[key] !== undefined ) {
|
|
57
|
+
options[key] = prompt.config[key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return options;
|
|
61
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const loadModelImpl = vi.fn();
|
|
4
|
+
const loadImageModelImpl = vi.fn();
|
|
5
|
+
const loadToolsImpl = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock( './ai_model.js', () => ( {
|
|
8
|
+
loadTextModel: ( ...args ) => loadModelImpl( ...args ),
|
|
9
|
+
loadImageModel: ( ...args ) => loadImageModelImpl( ...args ),
|
|
10
|
+
loadTools: ( ...args ) => loadToolsImpl( ...args )
|
|
11
|
+
} ) );
|
|
12
|
+
|
|
13
|
+
const importSut = async () => import( './ai_sdk_options.js' );
|
|
14
|
+
|
|
15
|
+
const makeTextPrompt = config => ( {
|
|
16
|
+
name: 'test@v1',
|
|
17
|
+
config: {
|
|
18
|
+
provider: 'anthropic',
|
|
19
|
+
model: 'claude-haiku-4-5',
|
|
20
|
+
...config
|
|
21
|
+
},
|
|
22
|
+
messages: [
|
|
23
|
+
{ role: 'system', content: 'You are concise.' },
|
|
24
|
+
{ role: 'user', content: 'Hello' }
|
|
25
|
+
],
|
|
26
|
+
instructions: null
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
const makeImagePrompt = config => ( {
|
|
30
|
+
name: 'image@v1',
|
|
31
|
+
config: {
|
|
32
|
+
provider: 'openai',
|
|
33
|
+
model: 'gpt-image-1',
|
|
34
|
+
...config
|
|
35
|
+
},
|
|
36
|
+
messages: [],
|
|
37
|
+
instructions: 'Generate a cinematic image of a NASCAR race at sunset.'
|
|
38
|
+
} );
|
|
39
|
+
|
|
40
|
+
describe( 'ai_sdk_options', () => {
|
|
41
|
+
beforeEach( () => {
|
|
42
|
+
vi.resetModules();
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
loadModelImpl.mockReturnValue( 'MODEL' );
|
|
45
|
+
loadImageModelImpl.mockReturnValue( 'IMAGE_MODEL' );
|
|
46
|
+
loadToolsImpl.mockReturnValue( null );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
it( 'maps loaded prompts to AI SDK text options', async () => {
|
|
50
|
+
const prompt = makeTextPrompt( {
|
|
51
|
+
temperature: 0.3,
|
|
52
|
+
maxTokens: 1000,
|
|
53
|
+
providerOptions: { anthropic: { effort: 'medium' } }
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
57
|
+
const result = loadAiSdkTextOptions( prompt );
|
|
58
|
+
|
|
59
|
+
expect( loadModelImpl ).toHaveBeenCalledWith( prompt );
|
|
60
|
+
expect( loadToolsImpl ).toHaveBeenCalledWith( prompt );
|
|
61
|
+
expect( result ).toEqual( {
|
|
62
|
+
model: 'MODEL',
|
|
63
|
+
messages: prompt.messages,
|
|
64
|
+
providerOptions: prompt.config.providerOptions,
|
|
65
|
+
temperature: 0.3,
|
|
66
|
+
maxOutputTokens: 1000
|
|
67
|
+
} );
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
it( 'preserves temperature 0 in text options', async () => {
|
|
71
|
+
const prompt = makeTextPrompt( { temperature: 0 } );
|
|
72
|
+
|
|
73
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
74
|
+
const result = loadAiSdkTextOptions( prompt );
|
|
75
|
+
|
|
76
|
+
expect( result.temperature ).toBe( 0 );
|
|
77
|
+
} );
|
|
78
|
+
|
|
79
|
+
it( 'adds provider tools when prompt config resolves tools', async () => {
|
|
80
|
+
const prompt = makeTextPrompt( { tools: { googleSearch: {} } } );
|
|
81
|
+
const tools = { googleSearch: { type: 'google-search-tool' } };
|
|
82
|
+
loadToolsImpl.mockReturnValue( tools );
|
|
83
|
+
|
|
84
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
85
|
+
const result = loadAiSdkTextOptions( prompt );
|
|
86
|
+
|
|
87
|
+
expect( result.tools ).toBe( tools );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'throws when text options receive a prompt without message blocks', async () => {
|
|
91
|
+
const prompt = makeImagePrompt();
|
|
92
|
+
|
|
93
|
+
const { loadAiSdkTextOptions } = await importSut();
|
|
94
|
+
|
|
95
|
+
expect( () => loadAiSdkTextOptions( prompt ) ).toThrow(
|
|
96
|
+
'Prompt "image@v1" has no chat-style messages.'
|
|
97
|
+
);
|
|
98
|
+
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
99
|
+
expect( loadToolsImpl ).not.toHaveBeenCalled();
|
|
100
|
+
} );
|
|
101
|
+
|
|
102
|
+
it( 'maps loaded prompts to AI SDK image options', async () => {
|
|
103
|
+
const images = [ Buffer.from( 'image-bytes' ) ];
|
|
104
|
+
const mask = Buffer.from( 'mask-bytes' );
|
|
105
|
+
const prompt = makeImagePrompt( {
|
|
106
|
+
n: 2,
|
|
107
|
+
maxImagesPerCall: 1,
|
|
108
|
+
size: '1024x1024',
|
|
109
|
+
aspectRatio: '1:1',
|
|
110
|
+
seed: 42,
|
|
111
|
+
temperature: 0.7,
|
|
112
|
+
maxTokens: 1000,
|
|
113
|
+
providerOptions: { openai: { quality: 'high' } }
|
|
114
|
+
} );
|
|
115
|
+
|
|
116
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
117
|
+
const result = loadAiSdkImageOptions( { prompt, images, mask } );
|
|
118
|
+
|
|
119
|
+
expect( loadImageModelImpl ).toHaveBeenCalledWith( prompt );
|
|
120
|
+
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
121
|
+
expect( loadToolsImpl ).not.toHaveBeenCalled();
|
|
122
|
+
expect( result ).toEqual( {
|
|
123
|
+
model: 'IMAGE_MODEL',
|
|
124
|
+
prompt: {
|
|
125
|
+
text: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
126
|
+
images,
|
|
127
|
+
mask
|
|
128
|
+
},
|
|
129
|
+
providerOptions: prompt.config.providerOptions,
|
|
130
|
+
n: 2,
|
|
131
|
+
maxImagesPerCall: 1,
|
|
132
|
+
size: '1024x1024',
|
|
133
|
+
aspectRatio: '1:1',
|
|
134
|
+
seed: 42
|
|
135
|
+
} );
|
|
136
|
+
expect( result.temperature ).toBeUndefined();
|
|
137
|
+
expect( result.maxOutputTokens ).toBeUndefined();
|
|
138
|
+
} );
|
|
139
|
+
|
|
140
|
+
it( 'omits undefined image options while preserving explicit 0 seed', async () => {
|
|
141
|
+
const prompt = makeImagePrompt( { seed: 0 } );
|
|
142
|
+
|
|
143
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
144
|
+
const result = loadAiSdkImageOptions( { prompt } );
|
|
145
|
+
|
|
146
|
+
expect( result ).toEqual( {
|
|
147
|
+
model: 'IMAGE_MODEL',
|
|
148
|
+
prompt: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
149
|
+
providerOptions: undefined,
|
|
150
|
+
seed: 0
|
|
151
|
+
} );
|
|
152
|
+
} );
|
|
153
|
+
|
|
154
|
+
it( 'throws when image options receive a prompt without instructions', async () => {
|
|
155
|
+
const prompt = makeTextPrompt();
|
|
156
|
+
|
|
157
|
+
const { loadAiSdkImageOptions } = await importSut();
|
|
158
|
+
|
|
159
|
+
expect( () => loadAiSdkImageOptions( { prompt } ) ).toThrow(
|
|
160
|
+
'Prompt "test@v1" has no instructions.'
|
|
161
|
+
);
|
|
162
|
+
expect( loadImageModelImpl ).not.toHaveBeenCalled();
|
|
163
|
+
} );
|
|
164
|
+
} );
|
package/src/cost/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export const calculateLLMCallCost = async ( { modelId, usage } ) => {
|
|
|
38
38
|
if ( Number.isFinite( pricing.output ) && Number.isFinite( outputTokens ) ) {
|
|
39
39
|
llmUsage.addUsage( { type: 'output', ppm: pricing.output, amount: outputTokens } );
|
|
40
40
|
}
|
|
41
|
-
// When there
|
|
41
|
+
// When there are no reasoning costs, providers do not differentiate reasoning vs output, so the price is included in the output
|
|
42
42
|
if ( Number.isFinite( pricing.reasoning ) && Number.isFinite( reasoningTokens ) ) {
|
|
43
43
|
llmUsage.addUsage( { type: 'reasoning', ppm: pricing.reasoning, amount: reasoningTokens } );
|
|
44
44
|
}
|