@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.
Files changed (47) hide show
  1. package/package.json +7 -3
  2. package/src/agent.js +7 -2
  3. package/src/generate.js +1 -0
  4. package/src/index.d.ts +14 -6
  5. package/src/utils/wrap.js +35 -2
  6. package/src/agent.spec.js +0 -595
  7. package/src/ai_provider.spec.js +0 -217
  8. package/src/ai_sdk_options.spec.js +0 -390
  9. package/src/fixtures/image_response_v7_google_vertex.js +0 -72
  10. package/src/fixtures/image_response_v7_openai.js +0 -77
  11. package/src/fixtures/models_api_light.json +0 -675
  12. package/src/fixtures/models_api_v7_subset.json +0 -259
  13. package/src/fixtures/stream_response_v7_anthropic.js +0 -389
  14. package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
  15. package/src/fixtures/stream_response_v7_openai.js +0 -353
  16. package/src/fixtures/stream_response_v7_perplexity.js +0 -671
  17. package/src/fixtures/text_response_v7_anthropic.js +0 -359
  18. package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
  19. package/src/fixtures/text_response_v7_google_vertex.js +0 -435
  20. package/src/fixtures/text_response_v7_openai.js +0 -347
  21. package/src/fixtures/text_response_v7_perplexity.js +0 -764
  22. package/src/generate.spec.js +0 -525
  23. package/src/prompt/content.spec.js +0 -208
  24. package/src/prompt/interpolations.spec.js +0 -109
  25. package/src/prompt/loader.full.spec.js +0 -309
  26. package/src/prompt/loader.spec.js +0 -258
  27. package/src/prompt/markup/attributes.spec.js +0 -132
  28. package/src/prompt/markup/nodes.spec.js +0 -333
  29. package/src/prompt/markup/tokenizer.spec.js +0 -149
  30. package/src/prompt/markup/tokens.spec.js +0 -250
  31. package/src/prompt/validations.spec.js +0 -812
  32. package/src/utils/cost.full.spec.js +0 -272
  33. package/src/utils/cost.spec.js +0 -290
  34. package/src/utils/error_handler.spec.js +0 -230
  35. package/src/utils/file.spec.js +0 -89
  36. package/src/utils/image.spec.js +0 -20
  37. package/src/utils/legacy_cost_attribute.spec.js +0 -278
  38. package/src/utils/models.spec.js +0 -119
  39. package/src/utils/models_pricing.spec.js +0 -162
  40. package/src/utils/skills.spec.js +0 -168
  41. package/src/utils/sources.spec.js +0 -122
  42. package/src/utils/stream.spec.js +0 -55
  43. package/src/utils/tools.spec.js +0 -167
  44. package/src/utils/usage.full.spec.js +0 -215
  45. package/src/utils/usage.spec.js +0 -246
  46. package/src/utils/wrap.spec.js +0 -467
  47. package/src/validations.spec.js +0 -631
@@ -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
- };