@output.ai/llm 0.2.7 → 0.2.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/llm",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/ai_sdk.js CHANGED
@@ -5,24 +5,25 @@ import * as AI from 'ai';
5
5
  import { validateGenerateTextArgs, validateGenerateObjectArgs, validateGenerateArrayArgs, validateGenerateEnumArgs } from './validations.js';
6
6
  import { loadPrompt } from './prompt_loader.js';
7
7
 
8
- /*
9
- Word of wisdom:
10
- We could retrieve the result object using the rest operator:
11
- ```js
12
- const { usage, providerMetadata, ...rest } = response;
13
- const result = rest[resultProperty];
14
- ```
15
- But we CAN'T because the response of the generateText is an instance of `DefaultGenerateTextResult`
16
- and 'text' is a getter (`get text()`).
17
- Be aware of this when refactoring.
18
- */
19
8
  const traceWrapper = async ( { traceId, resultProperty, fn } ) => {
20
9
  try {
21
10
  const response = await fn();
22
11
  const { usage, providerMetadata } = response;
23
12
  const result = response[resultProperty];
24
13
  Tracing.addEventEnd( { id: traceId, details: { result, usage, providerMetadata } } );
25
- return result;
14
+
15
+ // Use a Proxy to add 'result' as a unified field name without mutating the AI SDK response.
16
+ // This preserves the original response object (with its getters/prototype) while allowing
17
+ // developers to use 'result' consistently across all generate* functions.
18
+ // Note: Don't use spread/rest on response - AI SDK uses getters that won't copy correctly.
19
+ return new Proxy( response, {
20
+ get( target, prop, receiver ) {
21
+ if ( prop === 'result' ) {
22
+ return target[resultProperty];
23
+ }
24
+ return Reflect.get( target, prop, receiver );
25
+ }
26
+ } );
26
27
  } catch ( error ) {
27
28
  Tracing.addEventError( { id: traceId, details: error } );
28
29
  throw error;
@@ -63,7 +64,7 @@ const extraAiSdkOptionsFromPrompt = prompt => {
63
64
  * @param {Record<string, string | number>} [args.variables] - Variables to interpolate
64
65
  * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
65
66
  * @throws {FatalError} If the prompt file is not found or template rendering fails
66
- * @returns {Promise<string>} Generated text
67
+ * @returns {Promise<GenerateTextResult>} AI SDK response with text and metadata
67
68
  */
68
69
  export async function generateText( { prompt, variables } ) {
69
70
  validateGenerateTextArgs( { prompt, variables } );
@@ -87,7 +88,7 @@ export async function generateText( { prompt, variables } ) {
87
88
  * @param {string} [args.schemaDescription] - Output schema description
88
89
  * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
89
90
  * @throws {FatalError} If the prompt file is not found or template rendering fails
90
- * @returns {Promise<object>} Object matching the provided schema
91
+ * @returns {Promise<GenerateObjectResult>} AI SDK response with object and metadata
91
92
  */
92
93
  export async function generateObject( args ) {
93
94
  validateGenerateObjectArgs( args );
@@ -118,7 +119,7 @@ export async function generateObject( args ) {
118
119
  * @param {string} [args.schemaDescription] - Output schema description
119
120
  * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
120
121
  * @throws {FatalError} If the prompt file is not found or template rendering fails
121
- * @returns {Promise<object>} Array where each element matches the schema
122
+ * @returns {Promise<GenerateObjectResult>} AI SDK response with array and metadata
122
123
  */
123
124
  export async function generateArray( args ) {
124
125
  validateGenerateArrayArgs( args );
@@ -147,7 +148,7 @@ export async function generateArray( args ) {
147
148
  * @param {string[]} args.enum - Allowed values for the generation
148
149
  * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
149
150
  * @throws {FatalError} If the prompt file is not found or template rendering fails
150
- * @returns {Promise<string>} One of the provided enum values
151
+ * @returns {Promise<GenerateObjectResult>} AI SDK response with enum value and metadata
151
152
  */
152
153
  export async function generateEnum( args ) {
153
154
  validateGenerateEnumArgs( args );
@@ -52,8 +52,17 @@ beforeEach( () => {
52
52
  loadModelImpl.mockReset().mockReturnValue( 'MODEL' );
53
53
  loadPromptImpl.mockReset().mockReturnValue( basePrompt );
54
54
 
55
- aiFns.generateText.mockReset().mockResolvedValue( { text: 'TEXT' } );
56
- aiFns.generateObject.mockReset().mockResolvedValue( { object: 'OBJECT' } );
55
+ aiFns.generateText.mockReset().mockResolvedValue( {
56
+ text: 'TEXT',
57
+ sources: [],
58
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
59
+ finishReason: 'stop'
60
+ } );
61
+ aiFns.generateObject.mockReset().mockResolvedValue( {
62
+ object: 'OBJECT',
63
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
64
+ finishReason: 'stop'
65
+ } );
57
66
 
58
67
  validators.validateGenerateTextArgs.mockClear();
59
68
  validators.validateGenerateObjectArgs.mockClear();
@@ -83,12 +92,19 @@ describe( 'ai_sdk', () => {
83
92
  temperature: 0.3,
84
93
  providerOptions: basePrompt.config.providerOptions
85
94
  } );
86
- expect( result ).toBe( 'TEXT' );
95
+ expect( result.text ).toBe( 'TEXT' );
96
+ expect( result.sources ).toEqual( [] );
97
+ expect( result.usage ).toEqual( { inputTokens: 10, outputTokens: 5, totalTokens: 15 } );
98
+ expect( result.finishReason ).toBe( 'stop' );
87
99
  } );
88
100
 
89
101
  it( 'generateObject: validates, traces, calls AI with output object and returns object', async () => {
90
102
  const { generateObject } = await importSut();
91
- aiFns.generateObject.mockResolvedValueOnce( { object: { a: 1 } } );
103
+ aiFns.generateObject.mockResolvedValueOnce( {
104
+ object: { a: 1 },
105
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
106
+ finishReason: 'stop'
107
+ } );
92
108
 
93
109
  const schema = z.object( { a: z.number() } );
94
110
  const result = await generateObject( {
@@ -113,12 +129,16 @@ describe( 'ai_sdk', () => {
113
129
  temperature: 0.3,
114
130
  providerOptions: basePrompt.config.providerOptions
115
131
  } );
116
- expect( result ).toEqual( { a: 1 } );
132
+ expect( result.object ).toEqual( { a: 1 } );
117
133
  } );
118
134
 
119
135
  it( 'generateArray: validates, traces, calls AI (item schema) and returns array', async () => {
120
136
  const { generateArray } = await importSut();
121
- aiFns.generateObject.mockResolvedValueOnce( { object: [ 1, 2 ] } );
137
+ aiFns.generateObject.mockResolvedValueOnce( {
138
+ object: [ 1, 2 ],
139
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
140
+ finishReason: 'stop'
141
+ } );
122
142
 
123
143
  const schema = z.number();
124
144
  const result = await generateArray( {
@@ -143,12 +163,16 @@ describe( 'ai_sdk', () => {
143
163
  temperature: 0.3,
144
164
  providerOptions: basePrompt.config.providerOptions
145
165
  } );
146
- expect( result ).toEqual( [ 1, 2 ] );
166
+ expect( result.object ).toEqual( [ 1, 2 ] );
147
167
  } );
148
168
 
149
169
  it( 'generateEnum: validates, traces, calls AI with output enum and returns value', async () => {
150
170
  const { generateEnum } = await importSut();
151
- aiFns.generateObject.mockResolvedValueOnce( { object: 'B' } );
171
+ aiFns.generateObject.mockResolvedValueOnce( {
172
+ object: 'B',
173
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
174
+ finishReason: 'stop'
175
+ } );
152
176
 
153
177
  const result = await generateEnum( { prompt: 'test_prompt@v1', enum: [ 'A', 'B', 'C' ] } );
154
178
 
@@ -165,7 +189,7 @@ describe( 'ai_sdk', () => {
165
189
  temperature: 0.3,
166
190
  providerOptions: basePrompt.config.providerOptions
167
191
  } );
168
- expect( result ).toBe( 'B' );
192
+ expect( result.object ).toBe( 'B' );
169
193
  } );
170
194
 
171
195
  it( 'generateText: passes provider-specific options to AI SDK', async () => {
@@ -325,4 +349,149 @@ describe( 'ai_sdk', () => {
325
349
  }
326
350
  } );
327
351
  } );
352
+
353
+ it( 'generateText: passes through providerMetadata', async () => {
354
+ aiFns.generateText.mockResolvedValueOnce( {
355
+ text: 'TEXT',
356
+ sources: [],
357
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
358
+ finishReason: 'stop',
359
+ providerMetadata: { anthropic: { cacheReadInputTokens: 50 } }
360
+ } );
361
+
362
+ const { generateText } = await importSut();
363
+ const result = await generateText( { prompt: 'test_prompt@v1' } );
364
+
365
+ expect( result.providerMetadata ).toEqual( { anthropic: { cacheReadInputTokens: 50 } } );
366
+ } );
367
+
368
+ it( 'generateText: passes through warnings and response metadata', async () => {
369
+ aiFns.generateText.mockResolvedValueOnce( {
370
+ text: 'TEXT',
371
+ sources: [],
372
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
373
+ finishReason: 'stop',
374
+ warnings: [ { type: 'other', message: 'Test warning' } ],
375
+ response: { id: 'req_123', modelId: 'gpt-4o-2024-05-13' }
376
+ } );
377
+
378
+ const { generateText } = await importSut();
379
+ const result = await generateText( { prompt: 'test_prompt@v1' } );
380
+
381
+ expect( result.warnings ).toEqual( [ { type: 'other', message: 'Test warning' } ] );
382
+ expect( result.response ).toEqual( { id: 'req_123', modelId: 'gpt-4o-2024-05-13' } );
383
+ } );
384
+
385
+ it( 'generateText: includes unified result field that matches text', async () => {
386
+ const { generateText } = await importSut();
387
+ const response = await generateText( { prompt: 'test_prompt@v1' } );
388
+
389
+ expect( response.result ).toBe( 'TEXT' );
390
+ expect( response.result ).toBe( response.text );
391
+ } );
392
+
393
+ it( 'generateObject: includes unified result field that matches object', async () => {
394
+ const { generateObject } = await importSut();
395
+ aiFns.generateObject.mockResolvedValueOnce( {
396
+ object: { a: 1, b: 'test' },
397
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
398
+ finishReason: 'stop'
399
+ } );
400
+
401
+ const schema = z.object( { a: z.number(), b: z.string() } );
402
+ const response = await generateObject( { prompt: 'test_prompt@v1', schema } );
403
+
404
+ expect( response.result ).toEqual( { a: 1, b: 'test' } );
405
+ expect( response.result ).toEqual( response.object );
406
+ } );
407
+
408
+ it( 'generateArray: includes unified result field that matches object', async () => {
409
+ const { generateArray } = await importSut();
410
+ aiFns.generateObject.mockResolvedValueOnce( {
411
+ object: [ 'item1', 'item2', 'item3' ],
412
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
413
+ finishReason: 'stop'
414
+ } );
415
+
416
+ const schema = z.string();
417
+ const response = await generateArray( { prompt: 'test_prompt@v1', schema } );
418
+
419
+ expect( response.result ).toEqual( [ 'item1', 'item2', 'item3' ] );
420
+ expect( response.result ).toEqual( response.object );
421
+ } );
422
+
423
+ it( 'generateEnum: includes unified result field that matches object', async () => {
424
+ const { generateEnum } = await importSut();
425
+ aiFns.generateObject.mockResolvedValueOnce( {
426
+ object: 'yes',
427
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
428
+ finishReason: 'stop'
429
+ } );
430
+
431
+ const response = await generateEnum( { prompt: 'test_prompt@v1', enum: [ 'yes', 'no' ] } );
432
+
433
+ expect( response.result ).toBe( 'yes' );
434
+ expect( response.result ).toBe( response.object );
435
+ } );
436
+
437
+ it( 'generateText: traces error and rethrows when AI SDK fails', async () => {
438
+ const error = new Error( 'API rate limit exceeded' );
439
+ aiFns.generateText.mockRejectedValueOnce( error );
440
+ const { generateText } = await importSut();
441
+
442
+ await expect( generateText( { prompt: 'test_prompt@v1' } ) ).rejects.toThrow( 'API rate limit exceeded' );
443
+ expect( tracingSpies.addEventError ).toHaveBeenCalledWith(
444
+ expect.objectContaining( { details: error } )
445
+ );
446
+ } );
447
+
448
+ it( 'generateObject: traces error and rethrows when AI SDK fails', async () => {
449
+ const error = new Error( 'Invalid schema' );
450
+ aiFns.generateObject.mockRejectedValueOnce( error );
451
+ const { generateObject } = await importSut();
452
+
453
+ const schema = z.object( { a: z.number() } );
454
+ await expect( generateObject( { prompt: 'test_prompt@v1', schema } ) ).rejects.toThrow( 'Invalid schema' );
455
+ expect( tracingSpies.addEventError ).toHaveBeenCalledWith(
456
+ expect.objectContaining( { details: error } )
457
+ );
458
+ } );
459
+
460
+ it( 'generateText: Proxy correctly handles AI SDK response with getter', async () => {
461
+ const responseWithGetter = {
462
+ _internalText: 'TEXT_FROM_GETTER',
463
+ get text() {
464
+ return this._internalText;
465
+ },
466
+ sources: [],
467
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
468
+ finishReason: 'stop'
469
+ };
470
+ aiFns.generateText.mockResolvedValueOnce( responseWithGetter );
471
+
472
+ const { generateText } = await importSut();
473
+ const response = await generateText( { prompt: 'test_prompt@v1' } );
474
+
475
+ expect( response.text ).toBe( 'TEXT_FROM_GETTER' );
476
+ expect( response.result ).toBe( 'TEXT_FROM_GETTER' );
477
+ } );
478
+
479
+ it( 'generateObject: Proxy correctly handles AI SDK response with getter', async () => {
480
+ const responseWithGetter = {
481
+ _internalObject: { value: 42 },
482
+ get object() {
483
+ return this._internalObject;
484
+ },
485
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
486
+ finishReason: 'stop'
487
+ };
488
+ aiFns.generateObject.mockResolvedValueOnce( responseWithGetter );
489
+
490
+ const { generateObject } = await importSut();
491
+ const schema = z.object( { value: z.number() } );
492
+ const response = await generateObject( { prompt: 'test_prompt@v1', schema } );
493
+
494
+ expect( response.object ).toEqual( { value: 42 } );
495
+ expect( response.result ).toEqual( { value: 42 } );
496
+ } );
328
497
  } );
package/src/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import type { z } from '@output.ai/core';
2
+ import type {
3
+ GenerateTextResult as AIGenerateTextResult,
4
+ GenerateObjectResult as AIGenerateObjectResult
5
+ } from 'ai';
2
6
 
3
7
  /**
4
8
  * Represents a single message in a prompt conversation.
@@ -61,6 +65,36 @@ export type Prompt = {
61
65
  messages: PromptMessage[];
62
66
  };
63
67
 
68
+ // Re-export AI SDK types directly (auto-synced with AI SDK updates)
69
+ export type {
70
+ LanguageModelUsage,
71
+ FinishReason,
72
+ LanguageModelResponseMetadata,
73
+ ProviderMetadata,
74
+ CallWarning
75
+ } from 'ai';
76
+
77
+ /**
78
+ * Result from generateText including full AI SDK response metadata.
79
+ * Extends AI SDK's GenerateTextResult with a unified `result` field.
80
+ */
81
+ export type GenerateTextResult =
82
+ AIGenerateTextResult<Record<string, never>, unknown> & {
83
+ /** Unified field name alias for 'text' - provides consistency across all generate* functions */
84
+ result: string;
85
+ };
86
+
87
+ /**
88
+ * Result from generateObject/generateArray/generateEnum including full AI SDK response metadata.
89
+ * Extends AI SDK's GenerateObjectResult with a unified `result` field.
90
+ * @typeParam T - The type of the generated object, inferred from the schema parameter
91
+ */
92
+ export type GenerateObjectResult<T> =
93
+ AIGenerateObjectResult<T> & {
94
+ /** Unified field name alias for 'object' - provides consistency across all generate* functions */
95
+ result: T;
96
+ };
97
+
64
98
  /**
65
99
  * Loads a prompt file and interpolates variables into its content.
66
100
  *
@@ -82,14 +116,14 @@ export function loadPrompt(
82
116
  * @param args - Generation arguments.
83
117
  * @param args.prompt - Prompt file name.
84
118
  * @param args.variables - Variables to interpolate.
85
- * @returns Generated text.
119
+ * @returns AI SDK response with text and metadata.
86
120
  */
87
121
  export function generateText(
88
122
  args: {
89
123
  prompt: string,
90
124
  variables?: Record<string, string | number | boolean>
91
125
  }
92
- ): Promise<string>;
126
+ ): Promise<GenerateTextResult>;
93
127
 
94
128
  /**
95
129
  * Use an LLM model to generate an object with a fixed schema.
@@ -103,7 +137,7 @@ export function generateText(
103
137
  * @param args.schema - Output schema.
104
138
  * @param args.schemaName - Output schema name.
105
139
  * @param args.schemaDescription - Output schema description.
106
- * @returns Resolves to an object matching the provided schema.
140
+ * @returns AI SDK response with object and metadata.
107
141
  */
108
142
  export function generateObject<TSchema extends z.ZodObject>(
109
143
  args: {
@@ -113,7 +147,7 @@ export function generateObject<TSchema extends z.ZodObject>(
113
147
  schemaName?: string,
114
148
  schemaDescription?: string
115
149
  }
116
- ): Promise<z.infer<TSchema>>;
150
+ ): Promise<GenerateObjectResult<z.infer<TSchema>>>;
117
151
 
118
152
  /**
119
153
  * Use an LLM model to generate an array of values with a fixed schema.
@@ -127,7 +161,7 @@ export function generateObject<TSchema extends z.ZodObject>(
127
161
  * @param args.schema - Output schema (array item).
128
162
  * @param args.schemaName - Output schema name.
129
163
  * @param args.schemaDescription - Output schema description.
130
- * @returns Resolves to an array where each element matches the schema.
164
+ * @returns AI SDK response with array and metadata.
131
165
  */
132
166
  export function generateArray<TSchema extends z.ZodType>(
133
167
  args: {
@@ -137,7 +171,7 @@ export function generateArray<TSchema extends z.ZodType>(
137
171
  schemaName?: string,
138
172
  schemaDescription?: string
139
173
  }
140
- ): Promise<Array<z.infer<TSchema>>>;
174
+ ): Promise<GenerateObjectResult<Array<z.infer<TSchema>>>>;
141
175
 
142
176
  /**
143
177
  * Use an LLM model to generate a result from an enum (array of string values).
@@ -149,7 +183,7 @@ export function generateArray<TSchema extends z.ZodType>(
149
183
  * @param args.prompt - Prompt file name.
150
184
  * @param args.variables - Variables to interpolate.
151
185
  * @param args.enum - Allowed values for the generation.
152
- * @returns Resolves to one of the provided enum values.
186
+ * @returns AI SDK response with enum value and metadata.
153
187
  */
154
188
  export function generateEnum<const TEnum extends readonly [string, ...string[]]>(
155
189
  args: {
@@ -157,4 +191,4 @@ export function generateEnum<const TEnum extends readonly [string, ...string[]]>
157
191
  variables?: Record<string, string | number | boolean>,
158
192
  enum: TEnum
159
193
  }
160
- ): Promise<TEnum[number]>;
194
+ ): Promise<GenerateObjectResult<TEnum[number]>>;