@outputai/llm 0.11.1-next.da26845.0 → 0.12.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 +7 -3
- package/src/agent.js +7 -2
- package/src/generate.js +1 -0
- package/src/index.d.ts +14 -6
- package/src/utils/wrap.js +35 -2
- package/src/agent.spec.js +0 -595
- package/src/ai_provider.spec.js +0 -217
- package/src/ai_sdk_options.spec.js +0 -390
- package/src/fixtures/image_response_v7_google_vertex.js +0 -72
- package/src/fixtures/image_response_v7_openai.js +0 -77
- package/src/fixtures/models_api_light.json +0 -675
- package/src/fixtures/models_api_v7_subset.json +0 -259
- package/src/fixtures/stream_response_v7_anthropic.js +0 -389
- package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
- package/src/fixtures/stream_response_v7_openai.js +0 -353
- package/src/fixtures/stream_response_v7_perplexity.js +0 -671
- package/src/fixtures/text_response_v7_anthropic.js +0 -359
- package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
- package/src/fixtures/text_response_v7_google_vertex.js +0 -435
- package/src/fixtures/text_response_v7_openai.js +0 -347
- package/src/fixtures/text_response_v7_perplexity.js +0 -764
- package/src/generate.spec.js +0 -525
- package/src/prompt/content.spec.js +0 -208
- package/src/prompt/interpolations.spec.js +0 -109
- package/src/prompt/loader.full.spec.js +0 -309
- package/src/prompt/loader.spec.js +0 -258
- package/src/prompt/markup/attributes.spec.js +0 -132
- package/src/prompt/markup/nodes.spec.js +0 -333
- package/src/prompt/markup/tokenizer.spec.js +0 -149
- package/src/prompt/markup/tokens.spec.js +0 -250
- package/src/prompt/validations.spec.js +0 -812
- package/src/utils/cost.full.spec.js +0 -272
- package/src/utils/cost.spec.js +0 -290
- package/src/utils/error_handler.spec.js +0 -230
- package/src/utils/file.spec.js +0 -89
- package/src/utils/image.spec.js +0 -20
- package/src/utils/legacy_cost_attribute.spec.js +0 -278
- package/src/utils/models.spec.js +0 -119
- package/src/utils/models_pricing.spec.js +0 -162
- package/src/utils/skills.spec.js +0 -168
- package/src/utils/sources.spec.js +0 -122
- package/src/utils/stream.spec.js +0 -55
- package/src/utils/tools.spec.js +0 -167
- package/src/utils/usage.full.spec.js +0 -215
- package/src/utils/usage.spec.js +0 -246
- package/src/utils/wrap.spec.js +0 -467
- package/src/validations.spec.js +0 -631
package/src/ai_provider.spec.js
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const SHIPPED_PROVIDERS = [
|
|
4
|
-
{ name: 'amazon-bedrock', pkg: '@ai-sdk/amazon-bedrock', exportName: 'createAmazonBedrock' },
|
|
5
|
-
{ name: 'anthropic', pkg: '@ai-sdk/anthropic', exportName: 'createAnthropic' },
|
|
6
|
-
{ name: 'azure', pkg: '@ai-sdk/azure', exportName: 'createAzure' },
|
|
7
|
-
{ name: 'google-vertex', pkg: '@ai-sdk/google-vertex', exportName: 'createVertex' },
|
|
8
|
-
{ name: 'openai', pkg: '@ai-sdk/openai', exportName: 'createOpenAI' },
|
|
9
|
-
{ name: 'perplexity', pkg: '@ai-sdk/perplexity', exportName: 'createPerplexity' }
|
|
10
|
-
];
|
|
11
|
-
|
|
12
|
-
const makeProviderModules = () => Object.fromEntries(
|
|
13
|
-
SHIPPED_PROVIDERS.map( ( { name, pkg, exportName } ) => [
|
|
14
|
-
pkg,
|
|
15
|
-
{
|
|
16
|
-
[exportName]: vi.fn( options => ( { name, options } ) )
|
|
17
|
-
}
|
|
18
|
-
] )
|
|
19
|
-
);
|
|
20
|
-
|
|
21
|
-
const undiciSpies = vi.hoisted( () => ( {
|
|
22
|
-
EnvHttpProxyAgent: vi.fn(),
|
|
23
|
-
fetch: vi.fn()
|
|
24
|
-
} ) );
|
|
25
|
-
|
|
26
|
-
const importWithMockedProviders = async ( {
|
|
27
|
-
modules = makeProviderModules()
|
|
28
|
-
} = {} ) => {
|
|
29
|
-
await vi.resetModules();
|
|
30
|
-
|
|
31
|
-
for ( const { pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
32
|
-
vi.doMock( pkg, () => ( {
|
|
33
|
-
[exportName]: modules[pkg][exportName]
|
|
34
|
-
} ) );
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
vi.doMock( 'undici', async importOriginal => {
|
|
38
|
-
const actual = await importOriginal();
|
|
39
|
-
undiciSpies.EnvHttpProxyAgent.mockImplementation( function EnvHttpProxyAgent( options ) {
|
|
40
|
-
return new actual.EnvHttpProxyAgent( options );
|
|
41
|
-
} );
|
|
42
|
-
undiciSpies.fetch.mockResolvedValue( new actual.Response( null, { status: 204 } ) );
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
...actual,
|
|
46
|
-
EnvHttpProxyAgent: undiciSpies.EnvHttpProxyAgent,
|
|
47
|
-
fetch: undiciSpies.fetch
|
|
48
|
-
};
|
|
49
|
-
} );
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
modules,
|
|
53
|
-
...( await import( './ai_provider.js' ) )
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
afterEach( () => {
|
|
58
|
-
for ( const { pkg } of SHIPPED_PROVIDERS ) {
|
|
59
|
-
vi.doUnmock( pkg );
|
|
60
|
-
}
|
|
61
|
-
vi.doUnmock( 'undici' );
|
|
62
|
-
undiciSpies.EnvHttpProxyAgent.mockReset();
|
|
63
|
-
undiciSpies.fetch.mockReset();
|
|
64
|
-
vi.resetModules();
|
|
65
|
-
vi.restoreAllMocks();
|
|
66
|
-
} );
|
|
67
|
-
|
|
68
|
-
describe( 'getProvider', () => {
|
|
69
|
-
it( 'does not initialize shipped providers on import', async () => {
|
|
70
|
-
const { modules } = await importWithMockedProviders();
|
|
71
|
-
|
|
72
|
-
for ( const { pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
73
|
-
expect( modules[pkg][exportName] ).not.toHaveBeenCalled();
|
|
74
|
-
}
|
|
75
|
-
} );
|
|
76
|
-
|
|
77
|
-
it( 'initializes each shipped provider with custom fetch when requested', async () => {
|
|
78
|
-
const { modules, getProvider } = await importWithMockedProviders();
|
|
79
|
-
|
|
80
|
-
for ( const { name, pkg, exportName } of SHIPPED_PROVIDERS ) {
|
|
81
|
-
const provider = getProvider( name );
|
|
82
|
-
|
|
83
|
-
expect( provider ).toMatchObject( { name, options: { fetch: expect.any( Function ) } } );
|
|
84
|
-
expect( modules[pkg][exportName] ).toHaveBeenCalledWith( { fetch: expect.any( Function ) } );
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
expect( undiciSpies.EnvHttpProxyAgent ).toHaveBeenCalledWith( {
|
|
88
|
-
headersTimeout: 15 * 60 * 1000,
|
|
89
|
-
bodyTimeout: 15 * 60 * 1000,
|
|
90
|
-
allowH2: false
|
|
91
|
-
} );
|
|
92
|
-
} );
|
|
93
|
-
|
|
94
|
-
it( 'passes the custom dispatcher through injected fetch', async () => {
|
|
95
|
-
const { getProvider } = await importWithMockedProviders();
|
|
96
|
-
const provider = getProvider( 'openai' );
|
|
97
|
-
const init = { method: 'POST', body: 'body' };
|
|
98
|
-
|
|
99
|
-
await provider.options.fetch( 'https://api.example.test', init );
|
|
100
|
-
|
|
101
|
-
expect( undiciSpies.fetch ).toHaveBeenCalledWith(
|
|
102
|
-
'https://api.example.test',
|
|
103
|
-
{
|
|
104
|
-
...init,
|
|
105
|
-
dispatcher: undiciSpies.EnvHttpProxyAgent.mock.results[0].value
|
|
106
|
-
}
|
|
107
|
-
);
|
|
108
|
-
} );
|
|
109
|
-
|
|
110
|
-
it( 'allows injected fetch callers to override the dispatcher', async () => {
|
|
111
|
-
const { getProvider } = await importWithMockedProviders();
|
|
112
|
-
const provider = getProvider( 'openai' );
|
|
113
|
-
const dispatcher = { name: 'custom-dispatcher' };
|
|
114
|
-
|
|
115
|
-
await provider.options.fetch( 'https://api.example.test', { dispatcher } );
|
|
116
|
-
|
|
117
|
-
expect( undiciSpies.fetch ).toHaveBeenCalledWith(
|
|
118
|
-
'https://api.example.test',
|
|
119
|
-
{ dispatcher }
|
|
120
|
-
);
|
|
121
|
-
} );
|
|
122
|
-
|
|
123
|
-
it( 'can import and initialize all installed shipped providers', async () => {
|
|
124
|
-
const { getProvider } = await import( './ai_provider.js' );
|
|
125
|
-
|
|
126
|
-
for ( const { name } of SHIPPED_PROVIDERS ) {
|
|
127
|
-
expect( getProvider( name ) ).toEqual( expect.any( Function ) );
|
|
128
|
-
}
|
|
129
|
-
} );
|
|
130
|
-
|
|
131
|
-
it( 'caches initialized providers', async () => {
|
|
132
|
-
const { modules, getProvider } = await importWithMockedProviders();
|
|
133
|
-
|
|
134
|
-
const first = getProvider( 'openai' );
|
|
135
|
-
const second = getProvider( 'openai' );
|
|
136
|
-
|
|
137
|
-
expect( second ).toBe( first );
|
|
138
|
-
expect( modules['@ai-sdk/openai'].createOpenAI ).toHaveBeenCalledTimes( 1 );
|
|
139
|
-
} );
|
|
140
|
-
|
|
141
|
-
it( 'uses registered providers before shipped providers', async () => {
|
|
142
|
-
const { modules, getProvider, registerProvider } = await importWithMockedProviders();
|
|
143
|
-
const customProvider = vi.fn( model => ( { provider: 'custom', model } ) );
|
|
144
|
-
|
|
145
|
-
registerProvider( 'openai', customProvider );
|
|
146
|
-
|
|
147
|
-
expect( getProvider( 'openai' ) ).toBe( customProvider );
|
|
148
|
-
expect( modules['@ai-sdk/openai'].createOpenAI ).not.toHaveBeenCalled();
|
|
149
|
-
} );
|
|
150
|
-
|
|
151
|
-
it( 'throws FatalError for unsupported providers', async () => {
|
|
152
|
-
const { getProvider } = await importWithMockedProviders();
|
|
153
|
-
|
|
154
|
-
expect( () => getProvider( 'not-real' ) ).toThrow( 'Unsupported provider "not-real"' );
|
|
155
|
-
} );
|
|
156
|
-
|
|
157
|
-
it( 'throws a friendly error when provider initialization fails', async () => {
|
|
158
|
-
const modules = makeProviderModules();
|
|
159
|
-
modules['@ai-sdk/openai'].createOpenAI.mockImplementation( () => {
|
|
160
|
-
throw new Error( 'Missing OpenAI API key' );
|
|
161
|
-
} );
|
|
162
|
-
const { getProvider } = await importWithMockedProviders( { modules } );
|
|
163
|
-
|
|
164
|
-
expect( () => getProvider( 'openai' ) ).toThrow(
|
|
165
|
-
'Failed to initialize provider "openai": Missing OpenAI API key'
|
|
166
|
-
);
|
|
167
|
-
} );
|
|
168
|
-
} );
|
|
169
|
-
|
|
170
|
-
describe( 'registerProvider', () => {
|
|
171
|
-
it( 'registers custom providers', async () => {
|
|
172
|
-
const { getProvider, getProviderNames, registerProvider } = await importWithMockedProviders();
|
|
173
|
-
const customProvider = vi.fn();
|
|
174
|
-
|
|
175
|
-
registerProvider( 'custom', customProvider );
|
|
176
|
-
|
|
177
|
-
expect( getProvider( 'custom' ) ).toBe( customProvider );
|
|
178
|
-
expect( getProviderNames() ).toContain( 'custom' );
|
|
179
|
-
} );
|
|
180
|
-
|
|
181
|
-
it( 'validates provider registration arguments', async () => {
|
|
182
|
-
const { registerProvider } = await importWithMockedProviders();
|
|
183
|
-
|
|
184
|
-
expect( () => registerProvider( '', vi.fn() ) ).toThrow( 'Provider name must be a non-empty string' );
|
|
185
|
-
expect( () => registerProvider( 'custom', 'not-a-function' ) ).toThrow( 'expected function, received string' );
|
|
186
|
-
} );
|
|
187
|
-
|
|
188
|
-
it.each( [
|
|
189
|
-
[ 'vertex', 'google-vertex' ],
|
|
190
|
-
[ 'bedrock', 'amazon-bedrock' ]
|
|
191
|
-
] )( 'rejects registering deprecated provider alias %s', async ( alias, canonical ) => {
|
|
192
|
-
const { registerProvider } = await importWithMockedProviders();
|
|
193
|
-
|
|
194
|
-
expect( () => registerProvider( alias, vi.fn() ) ).toThrow(
|
|
195
|
-
`Cannot register provider "${alias}": that name is a deprecated alias for "${canonical}". Register "${canonical}" instead.`
|
|
196
|
-
);
|
|
197
|
-
} );
|
|
198
|
-
} );
|
|
199
|
-
|
|
200
|
-
describe( 'getProviderNames', () => {
|
|
201
|
-
it( 'returns shipped and registered provider names without duplicates', async () => {
|
|
202
|
-
const { getProviderNames, registerProvider } = await importWithMockedProviders();
|
|
203
|
-
|
|
204
|
-
registerProvider( 'custom', vi.fn() );
|
|
205
|
-
registerProvider( 'openai', vi.fn() );
|
|
206
|
-
|
|
207
|
-
expect( getProviderNames() ).toEqual( [
|
|
208
|
-
'amazon-bedrock',
|
|
209
|
-
'anthropic',
|
|
210
|
-
'azure',
|
|
211
|
-
'google-vertex',
|
|
212
|
-
'openai',
|
|
213
|
-
'perplexity',
|
|
214
|
-
'custom'
|
|
215
|
-
] );
|
|
216
|
-
} );
|
|
217
|
-
} );
|
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const loadModelImpl = vi.fn();
|
|
4
|
-
const loadImageModelImpl = vi.fn();
|
|
5
|
-
const loadPromptToolsImpl = vi.fn();
|
|
6
|
-
const buildLoadSkillToolImpl = vi.fn();
|
|
7
|
-
|
|
8
|
-
vi.mock( './utils/models.js', () => ( {
|
|
9
|
-
loadTextModel: ( ...args ) => loadModelImpl( ...args ),
|
|
10
|
-
loadImageModel: ( ...args ) => loadImageModelImpl( ...args )
|
|
11
|
-
} ) );
|
|
12
|
-
|
|
13
|
-
vi.mock( './utils/tools.js', () => ( {
|
|
14
|
-
buildLoadSkillTool: ( ...args ) => buildLoadSkillToolImpl( ...args ),
|
|
15
|
-
loadPromptTools: ( ...args ) => loadPromptToolsImpl( ...args )
|
|
16
|
-
} ) );
|
|
17
|
-
|
|
18
|
-
vi.mock( 'ai', () => ( {
|
|
19
|
-
isStepCount: count => ( { type: 'step-count', count } )
|
|
20
|
-
} ) );
|
|
21
|
-
|
|
22
|
-
const importSut = async () => import( './ai_sdk_options.js' );
|
|
23
|
-
|
|
24
|
-
const makeTextPrompt = config => ( {
|
|
25
|
-
name: 'test@v1',
|
|
26
|
-
config: {
|
|
27
|
-
provider: 'anthropic',
|
|
28
|
-
model: 'claude-haiku-4-5',
|
|
29
|
-
maxSteps: 10,
|
|
30
|
-
...config
|
|
31
|
-
},
|
|
32
|
-
messages: [
|
|
33
|
-
{ role: 'system', content: 'You are concise.' },
|
|
34
|
-
{ role: 'user', content: 'Hello' }
|
|
35
|
-
],
|
|
36
|
-
instructions: null
|
|
37
|
-
} );
|
|
38
|
-
|
|
39
|
-
const makeImagePrompt = config => ( {
|
|
40
|
-
name: 'image@v1',
|
|
41
|
-
config: {
|
|
42
|
-
provider: 'openai',
|
|
43
|
-
model: 'gpt-image-1',
|
|
44
|
-
...config
|
|
45
|
-
},
|
|
46
|
-
messages: [],
|
|
47
|
-
instructions: 'Generate a cinematic image of a NASCAR race at sunset.'
|
|
48
|
-
} );
|
|
49
|
-
|
|
50
|
-
const writerSkill = {
|
|
51
|
-
name: 'writer',
|
|
52
|
-
description: 'Writes copy',
|
|
53
|
-
instructions: 'Do it.'
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const skillsCatalog =
|
|
57
|
-
'Available skills (use load_skill to get full instructions):\n- writer: Writes copy';
|
|
58
|
-
|
|
59
|
-
describe( 'ai_sdk_options', () => {
|
|
60
|
-
beforeEach( () => {
|
|
61
|
-
vi.resetModules();
|
|
62
|
-
vi.clearAllMocks();
|
|
63
|
-
loadModelImpl.mockReturnValue( 'MODEL' );
|
|
64
|
-
loadImageModelImpl.mockReturnValue( 'IMAGE_MODEL' );
|
|
65
|
-
loadPromptToolsImpl.mockReturnValue( null );
|
|
66
|
-
buildLoadSkillToolImpl.mockImplementation( skills => ( { type: 'load_skill', skills } ) );
|
|
67
|
-
} );
|
|
68
|
-
|
|
69
|
-
describe( 'loadAiSdkTextOptions', () => {
|
|
70
|
-
const loadText = async args => {
|
|
71
|
-
const { loadAiSdkTextOptions } = await importSut();
|
|
72
|
-
return loadAiSdkTextOptions( { skills: [], ...args } );
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
it( 'maps a loaded prompt to model, instructions, messages, and generation config', async () => {
|
|
76
|
-
const prompt = makeTextPrompt( {
|
|
77
|
-
temperature: 0.3,
|
|
78
|
-
maxOutputTokens: 1000,
|
|
79
|
-
providerOptions: { anthropic: { effort: 'medium' } }
|
|
80
|
-
} );
|
|
81
|
-
|
|
82
|
-
const result = await loadText( { prompt } );
|
|
83
|
-
|
|
84
|
-
expect( loadModelImpl ).toHaveBeenCalledWith( prompt );
|
|
85
|
-
expect( loadPromptToolsImpl ).toHaveBeenCalledWith( prompt );
|
|
86
|
-
expect( result ).toEqual( {
|
|
87
|
-
allowSystemInMessages: true,
|
|
88
|
-
maxRetries: 0,
|
|
89
|
-
model: 'MODEL',
|
|
90
|
-
instructions: [ { role: 'system', content: 'You are concise.' } ],
|
|
91
|
-
messages: [ { role: 'user', content: 'Hello' } ],
|
|
92
|
-
providerOptions: prompt.config.providerOptions,
|
|
93
|
-
temperature: 0.3,
|
|
94
|
-
maxOutputTokens: 1000
|
|
95
|
-
} );
|
|
96
|
-
} );
|
|
97
|
-
|
|
98
|
-
it( 'forwards all supported text generation options', async () => {
|
|
99
|
-
const generationOptions = {
|
|
100
|
-
frequencyPenalty: 0.2,
|
|
101
|
-
maxOutputTokens: 2000,
|
|
102
|
-
presencePenalty: 0.3,
|
|
103
|
-
providerOptions: { openai: { reasoningEffort: 'low' } },
|
|
104
|
-
seed: 42,
|
|
105
|
-
stopSequences: [ 'END' ],
|
|
106
|
-
temperature: 0.4,
|
|
107
|
-
topK: 40,
|
|
108
|
-
topP: 0.9
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const result = await loadText( { prompt: makeTextPrompt( generationOptions ) } );
|
|
112
|
-
|
|
113
|
-
expect( result ).toMatchObject( generationOptions );
|
|
114
|
-
} );
|
|
115
|
-
|
|
116
|
-
it( 'preserves temperature 0', async () => {
|
|
117
|
-
const result = await loadText( { prompt: makeTextPrompt( { temperature: 0 } ) } );
|
|
118
|
-
|
|
119
|
-
expect( result.temperature ).toBe( 0 );
|
|
120
|
-
} );
|
|
121
|
-
|
|
122
|
-
it( 'throws when the prompt has no chat-style messages', async () => {
|
|
123
|
-
const { loadAiSdkTextOptions } = await importSut();
|
|
124
|
-
|
|
125
|
-
expect( () => loadAiSdkTextOptions( { prompt: makeImagePrompt(), skills: [] } ) ).toThrow(
|
|
126
|
-
'Prompt "image@v1" has no chat-style messages.'
|
|
127
|
-
);
|
|
128
|
-
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
129
|
-
expect( loadPromptToolsImpl ).not.toHaveBeenCalled();
|
|
130
|
-
} );
|
|
131
|
-
|
|
132
|
-
it( 'forwards per-message providerOptions when splitting system and messages', async () => {
|
|
133
|
-
const providerOptions = { anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } } };
|
|
134
|
-
const prompt = {
|
|
135
|
-
name: 'cache@v1',
|
|
136
|
-
config: {
|
|
137
|
-
provider: 'anthropic',
|
|
138
|
-
model: 'claude-sonnet-4-5'
|
|
139
|
-
},
|
|
140
|
-
messages: [
|
|
141
|
-
{ role: 'system', content: 'Static', providerOptions },
|
|
142
|
-
{ role: 'user', content: 'Hello' }
|
|
143
|
-
],
|
|
144
|
-
instructions: null
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const result = await loadText( { prompt } );
|
|
148
|
-
|
|
149
|
-
expect( result.instructions ).toEqual( [ {
|
|
150
|
-
role: 'system',
|
|
151
|
-
content: 'Static',
|
|
152
|
-
providerOptions
|
|
153
|
-
} ] );
|
|
154
|
-
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
155
|
-
} );
|
|
156
|
-
|
|
157
|
-
it( 'returns an empty instructions array when the prompt has no system block', async () => {
|
|
158
|
-
const prompt = {
|
|
159
|
-
name: 'no-system@v1',
|
|
160
|
-
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
161
|
-
messages: [ { role: 'user', content: 'Hello' } ],
|
|
162
|
-
instructions: null
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const result = await loadText( { prompt } );
|
|
166
|
-
|
|
167
|
-
expect( result.instructions ).toEqual( [] );
|
|
168
|
-
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
169
|
-
} );
|
|
170
|
-
|
|
171
|
-
it( 'groups multiple system blocks into instructions and keeps them out of messages', async () => {
|
|
172
|
-
const prompt = {
|
|
173
|
-
name: 'multi-system@v1',
|
|
174
|
-
config: { provider: 'anthropic', model: 'claude-haiku-4-5' },
|
|
175
|
-
messages: [
|
|
176
|
-
{ role: 'system', content: 'First' },
|
|
177
|
-
{ role: 'system', content: 'Second' },
|
|
178
|
-
{ role: 'user', content: 'Hello' }
|
|
179
|
-
],
|
|
180
|
-
instructions: null
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const result = await loadText( { prompt } );
|
|
184
|
-
|
|
185
|
-
expect( result.instructions ).toEqual( [
|
|
186
|
-
{ role: 'system', content: 'First' },
|
|
187
|
-
{ role: 'system', content: 'Second' }
|
|
188
|
-
] );
|
|
189
|
-
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
190
|
-
} );
|
|
191
|
-
|
|
192
|
-
it( 'omits tools and stopWhen when no tools are present', async () => {
|
|
193
|
-
const result = await loadText( { prompt: makeTextPrompt() } );
|
|
194
|
-
|
|
195
|
-
expect( result.tools ).toBeUndefined();
|
|
196
|
-
expect( result.stopWhen ).toBeUndefined();
|
|
197
|
-
expect( buildLoadSkillToolImpl ).not.toHaveBeenCalled();
|
|
198
|
-
} );
|
|
199
|
-
|
|
200
|
-
it( 'sets prompt tools and stopWhen from maxSteps', async () => {
|
|
201
|
-
const promptTools = { googleSearch: { type: 'google-search-tool' } };
|
|
202
|
-
loadPromptToolsImpl.mockReturnValue( promptTools );
|
|
203
|
-
|
|
204
|
-
const result = await loadText( { prompt: makeTextPrompt( { tools: { googleSearch: {} }, maxSteps: 4 } ) } );
|
|
205
|
-
|
|
206
|
-
expect( result.tools ).toEqual( promptTools );
|
|
207
|
-
expect( result.stopWhen ).toEqual( { type: 'step-count', count: 4 } );
|
|
208
|
-
} );
|
|
209
|
-
|
|
210
|
-
it( 'lets caller tools override prompt tools on the same key', async () => {
|
|
211
|
-
loadPromptToolsImpl.mockReturnValue( {
|
|
212
|
-
googleSearch: { from: 'prompt' },
|
|
213
|
-
urlContext: { from: 'prompt' }
|
|
214
|
-
} );
|
|
215
|
-
|
|
216
|
-
const result = await loadText( {
|
|
217
|
-
prompt: makeTextPrompt(),
|
|
218
|
-
tools: { googleSearch: { from: 'caller' } }
|
|
219
|
-
} );
|
|
220
|
-
|
|
221
|
-
expect( result.tools ).toEqual( {
|
|
222
|
-
googleSearch: { from: 'caller' },
|
|
223
|
-
urlContext: { from: 'prompt' }
|
|
224
|
-
} );
|
|
225
|
-
} );
|
|
226
|
-
|
|
227
|
-
it( 'appends the skills catalog to the first system message and adds load_skill', async () => {
|
|
228
|
-
const providerOptions = { anthropic: { cacheControl: { type: 'ephemeral' } } };
|
|
229
|
-
const prompt = makeTextPrompt();
|
|
230
|
-
prompt.messages[0] = { ...prompt.messages[0], providerOptions };
|
|
231
|
-
|
|
232
|
-
const result = await loadText( {
|
|
233
|
-
prompt,
|
|
234
|
-
skills: [ writerSkill ]
|
|
235
|
-
} );
|
|
236
|
-
|
|
237
|
-
expect( buildLoadSkillToolImpl ).toHaveBeenCalledWith( [ writerSkill ] );
|
|
238
|
-
expect( result.instructions ).toEqual( [
|
|
239
|
-
{ role: 'system', content: `You are concise.\n\n${skillsCatalog}`, providerOptions }
|
|
240
|
-
] );
|
|
241
|
-
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
242
|
-
expect( result.tools ).toEqual( {
|
|
243
|
-
load_skill: { type: 'load_skill', skills: [ writerSkill ] }
|
|
244
|
-
} );
|
|
245
|
-
expect( result.stopWhen ).toEqual( { type: 'step-count', count: 10 } );
|
|
246
|
-
} );
|
|
247
|
-
|
|
248
|
-
it( 'adds a system message for the skills catalog when none exists', async () => {
|
|
249
|
-
const prompt = {
|
|
250
|
-
name: 'no-system@v1',
|
|
251
|
-
config: { provider: 'anthropic', model: 'claude-haiku-4-5', maxSteps: 10 },
|
|
252
|
-
messages: [ { role: 'user', content: 'Hello' } ],
|
|
253
|
-
instructions: null
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const result = await loadText( { prompt, skills: [ writerSkill ] } );
|
|
257
|
-
|
|
258
|
-
expect( result.instructions ).toEqual( [ { role: 'system', content: skillsCatalog } ] );
|
|
259
|
-
expect( result.messages ).toEqual( [ { role: 'user', content: 'Hello' } ] );
|
|
260
|
-
} );
|
|
261
|
-
|
|
262
|
-
it( 'keeps load_skill when the caller uses the same key', async () => {
|
|
263
|
-
const result = await loadText( {
|
|
264
|
-
prompt: makeTextPrompt(),
|
|
265
|
-
tools: { load_skill: { from: 'caller' }, search: { from: 'caller' } },
|
|
266
|
-
skills: [ writerSkill ]
|
|
267
|
-
} );
|
|
268
|
-
|
|
269
|
-
expect( result.tools ).toEqual( {
|
|
270
|
-
load_skill: { type: 'load_skill', skills: [ writerSkill ] },
|
|
271
|
-
search: { from: 'caller' }
|
|
272
|
-
} );
|
|
273
|
-
} );
|
|
274
|
-
|
|
275
|
-
it( 'forwards output, abortSignal, and caller stopWhen', async () => {
|
|
276
|
-
const output = { type: 'object' };
|
|
277
|
-
const abortSignal = AbortSignal.abort();
|
|
278
|
-
const stopWhen = { type: 'custom-stop' };
|
|
279
|
-
|
|
280
|
-
const result = await loadText( {
|
|
281
|
-
prompt: makeTextPrompt(),
|
|
282
|
-
output,
|
|
283
|
-
abortSignal,
|
|
284
|
-
stopWhen
|
|
285
|
-
} );
|
|
286
|
-
|
|
287
|
-
expect( result.output ).toBe( output );
|
|
288
|
-
expect( result.abortSignal ).toBe( abortSignal );
|
|
289
|
-
expect( result.stopWhen ).toBe( stopWhen );
|
|
290
|
-
} );
|
|
291
|
-
|
|
292
|
-
it( 'does not replace caller stopWhen with maxSteps when tools are present', async () => {
|
|
293
|
-
loadPromptToolsImpl.mockReturnValue( { googleSearch: { type: 'google-search-tool' } } );
|
|
294
|
-
const stopWhen = { type: 'custom-stop' };
|
|
295
|
-
|
|
296
|
-
const result = await loadText( {
|
|
297
|
-
prompt: makeTextPrompt( { maxSteps: 4 } ),
|
|
298
|
-
stopWhen
|
|
299
|
-
} );
|
|
300
|
-
|
|
301
|
-
expect( result.stopWhen ).toBe( stopWhen );
|
|
302
|
-
} );
|
|
303
|
-
|
|
304
|
-
it( 'sets toolChoice only when tools are present', async () => {
|
|
305
|
-
const withoutTools = await loadText( {
|
|
306
|
-
prompt: makeTextPrompt(),
|
|
307
|
-
toolChoice: 'required'
|
|
308
|
-
} );
|
|
309
|
-
expect( withoutTools.toolChoice ).toBeUndefined();
|
|
310
|
-
|
|
311
|
-
loadPromptToolsImpl.mockReturnValue( { googleSearch: { type: 'google-search-tool' } } );
|
|
312
|
-
const withTools = await loadText( {
|
|
313
|
-
prompt: makeTextPrompt(),
|
|
314
|
-
toolChoice: 'required'
|
|
315
|
-
} );
|
|
316
|
-
expect( withTools.toolChoice ).toBe( 'required' );
|
|
317
|
-
} );
|
|
318
|
-
} );
|
|
319
|
-
|
|
320
|
-
describe( 'loadAiSdkImageOptions', () => {
|
|
321
|
-
it( 'maps a loaded prompt to image options and ignores text generation config', async () => {
|
|
322
|
-
const images = [ Buffer.from( 'image-bytes' ) ];
|
|
323
|
-
const mask = Buffer.from( 'mask-bytes' );
|
|
324
|
-
const prompt = makeImagePrompt( {
|
|
325
|
-
n: 2,
|
|
326
|
-
maxImagesPerCall: 1,
|
|
327
|
-
size: '1024x1024',
|
|
328
|
-
aspectRatio: '1:1',
|
|
329
|
-
seed: 42,
|
|
330
|
-
temperature: 0.7,
|
|
331
|
-
maxOutputTokens: 1000,
|
|
332
|
-
providerOptions: { openai: { quality: 'high' } }
|
|
333
|
-
} );
|
|
334
|
-
|
|
335
|
-
const { loadAiSdkImageOptions } = await importSut();
|
|
336
|
-
const result = loadAiSdkImageOptions( { prompt, images, mask } );
|
|
337
|
-
|
|
338
|
-
expect( loadImageModelImpl ).toHaveBeenCalledWith( prompt );
|
|
339
|
-
expect( loadModelImpl ).not.toHaveBeenCalled();
|
|
340
|
-
expect( loadPromptToolsImpl ).not.toHaveBeenCalled();
|
|
341
|
-
expect( result ).toEqual( {
|
|
342
|
-
maxRetries: 0,
|
|
343
|
-
model: 'IMAGE_MODEL',
|
|
344
|
-
prompt: {
|
|
345
|
-
text: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
346
|
-
images,
|
|
347
|
-
mask
|
|
348
|
-
},
|
|
349
|
-
providerOptions: prompt.config.providerOptions,
|
|
350
|
-
n: 2,
|
|
351
|
-
maxImagesPerCall: 1,
|
|
352
|
-
size: '1024x1024',
|
|
353
|
-
aspectRatio: '1:1',
|
|
354
|
-
seed: 42
|
|
355
|
-
} );
|
|
356
|
-
expect( result.temperature ).toBeUndefined();
|
|
357
|
-
expect( result.maxOutputTokens ).toBeUndefined();
|
|
358
|
-
} );
|
|
359
|
-
|
|
360
|
-
it( 'omits undefined image options while preserving explicit 0 seed', async () => {
|
|
361
|
-
const { loadAiSdkImageOptions } = await importSut();
|
|
362
|
-
const result = loadAiSdkImageOptions( { prompt: makeImagePrompt( { seed: 0 } ) } );
|
|
363
|
-
|
|
364
|
-
expect( result ).toEqual( {
|
|
365
|
-
maxRetries: 0,
|
|
366
|
-
model: 'IMAGE_MODEL',
|
|
367
|
-
prompt: 'Generate a cinematic image of a NASCAR race at sunset.',
|
|
368
|
-
providerOptions: undefined,
|
|
369
|
-
seed: 0
|
|
370
|
-
} );
|
|
371
|
-
} );
|
|
372
|
-
|
|
373
|
-
it( 'throws when the prompt has no instructions', async () => {
|
|
374
|
-
const { loadAiSdkImageOptions } = await importSut();
|
|
375
|
-
|
|
376
|
-
expect( () => loadAiSdkImageOptions( { prompt: makeTextPrompt() } ) ).toThrow(
|
|
377
|
-
'Prompt "test@v1" has no instructions.'
|
|
378
|
-
);
|
|
379
|
-
expect( loadImageModelImpl ).not.toHaveBeenCalled();
|
|
380
|
-
} );
|
|
381
|
-
|
|
382
|
-
it( 'forwards abortSignal', async () => {
|
|
383
|
-
const abortSignal = AbortSignal.abort();
|
|
384
|
-
const { loadAiSdkImageOptions } = await importSut();
|
|
385
|
-
const result = loadAiSdkImageOptions( { prompt: makeImagePrompt(), abortSignal } );
|
|
386
|
-
|
|
387
|
-
expect( result.abortSignal ).toBe( abortSignal );
|
|
388
|
-
} );
|
|
389
|
-
} );
|
|
390
|
-
} );
|
|
@@ -1,72 +0,0 @@
|
|
|
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
|
-
};
|