@outputai/llm 0.1.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/src/ai_sdk.js ADDED
@@ -0,0 +1,145 @@
1
+ import { Tracing, emitEvent } from '@outputai/core/sdk_activity_integration';
2
+ import { loadModel, loadTools } from './ai_model.js';
3
+ import * as AI from 'ai';
4
+ import { validateGenerateTextArgs, validateStreamTextArgs } from './validations.js';
5
+ import { loadPrompt } from './prompt_loader.js';
6
+ import { extractSourcesFromSteps } from './source_extraction.js';
7
+ import { calculateLLMCallCost } from './cost/index.js';
8
+
9
+ // Starts the LLM trace, with the start event
10
+ const startTrace = ( name, details ) => {
11
+ const traceId = `${name}-${Date.now()}`;
12
+ Tracing.addEventStart( { kind: 'llm', name, id: traceId, details } );
13
+ return traceId;
14
+ };
15
+
16
+ const loadAiSdkOptionsFromPrompt = prompt => {
17
+ const options = {
18
+ model: loadModel( prompt ),
19
+ messages: prompt.messages,
20
+ providerOptions: prompt.config.providerOptions
21
+ };
22
+
23
+ if ( Number.isFinite( prompt.config.temperature ) ) {
24
+ options.temperature = prompt.config.temperature;
25
+ }
26
+
27
+ if ( prompt.config.maxTokens ) {
28
+ options.maxOutputTokens = prompt.config.maxTokens;
29
+ }
30
+
31
+ const tools = loadTools( prompt );
32
+ if ( tools ) {
33
+ options.tools = tools;
34
+ }
35
+
36
+ return options;
37
+ };
38
+
39
+ /**
40
+ * Use an LLM model to generate text.
41
+ *
42
+ * Accepts additional AI SDK options (tools, maxRetries, seed, etc.) that are passed through
43
+ * to the underlying provider. Options from the prompt file can be overridden at call time.
44
+ *
45
+ * @param {object} args - Generation arguments
46
+ * @param {string} args.prompt - Prompt file name
47
+ * @param {Record<string, string | number>} [args.variables] - Variables to interpolate
48
+ * @param {object} [args.tools] - AI SDK tools the model can call
49
+ * @param {'auto'|'none'|'required'|object} [args.toolChoice] - Tool selection strategy
50
+ * @param {number} [args.maxRetries] - Max retry attempts (default: 2)
51
+ * @param {number} [args.seed] - Seed for deterministic output
52
+ * @param {AbortSignal} [args.abortSignal] - Signal to abort the request
53
+ * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
54
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
55
+ * @returns {Promise<GenerateTextResult>} AI SDK response with text, toolCalls, and metadata
56
+ */
57
+ export async function generateText( { prompt, variables, promptDir, ...extraAiSdkOptions } ) {
58
+ validateGenerateTextArgs( { prompt, variables } );
59
+ const loadedPrompt = promptDir ? loadPrompt( prompt, variables, promptDir ) : loadPrompt( prompt, variables );
60
+ const traceId = startTrace( 'generateText', { prompt, variables, loadedPrompt } );
61
+ const { model: modelId } = loadedPrompt.config;
62
+
63
+ try {
64
+ const response = await AI.generateText( {
65
+ ...loadAiSdkOptionsFromPrompt( loadedPrompt ),
66
+ ...extraAiSdkOptions
67
+ } );
68
+ const { text: result, totalUsage: usage, providerMetadata } = response;
69
+ const sourcesFromTools = extractSourcesFromSteps( response.steps );
70
+ const cost = await calculateLLMCallCost( { usage, modelId } );
71
+
72
+ emitEvent( 'llm:call_cost', { modelId, cost, usage } );
73
+ Tracing.addEventEnd( { id: traceId, details: { result, usage, cost, providerMetadata, sourcesFromTools } } );
74
+
75
+ // Creates a proxy over the response from AI SDK to add
76
+ // - result: a shortcut for the actual SDK response (eg. .text);
77
+ // - sources: a way to retrieve the computed sources;
78
+ // It uses proxies instead of spreading the response object because AI SDK uses getters
79
+ // (and they don't deconstruct properly in JS).
80
+ return new Proxy( response, {
81
+ get( target, prop, receiver ) {
82
+ if ( prop === 'result' ) {
83
+ return target.text;
84
+ }
85
+ if ( prop === 'sources' && sourcesFromTools.length > 0 ) {
86
+ const responseSources = Array.isArray( target[prop] ) ? target[prop] : [];
87
+ const byUrl = new Map( [ ...sourcesFromTools, ...responseSources ].map( s => [ s.url, s ] ) );
88
+ return [ ...byUrl.values() ];
89
+ }
90
+ return Reflect.get( target, prop, receiver );
91
+ }
92
+ } );
93
+ } catch ( error ) {
94
+ Tracing.addEventError( { id: traceId, details: error } );
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Use an LLM model to stream text generation.
101
+ *
102
+ * Accepts additional AI SDK options (tools, onChunk, onFinish, onError, etc.) that are passed
103
+ * through to the underlying provider. Options from the prompt file can be overridden at call time.
104
+ *
105
+ * @param {object} args - Generation arguments
106
+ * @param {string} args.prompt - Prompt file name
107
+ * @param {Record<string, string | number>} [args.variables] - Variables to interpolate
108
+ * @param {object} [args.tools] - AI SDK tools the model can call
109
+ * @param {'auto'|'none'|'required'|object} [args.toolChoice] - Tool selection strategy
110
+ * @param {number} [args.maxRetries] - Max retry attempts (default: 2)
111
+ * @param {AbortSignal} [args.abortSignal] - Signal to abort the request
112
+ * @param {Function} [args.onChunk] - Callback for each stream chunk
113
+ * @param {Function} [args.onFinish] - Callback when stream finishes (called after internal tracing)
114
+ * @param {Function} [args.onError] - Callback when stream errors (called after internal tracing)
115
+ * @throws {ValidationError} If required arguments are missing or prompt file has invalid config
116
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
117
+ * @returns {AIStreamTextResult} AI SDK stream result with textStream, fullStream, and metadata promises (synchronous)
118
+ */
119
+ export function streamText( { prompt, variables, onFinish: userOnFinish, onError: userOnError, ...restOptions } ) {
120
+ validateStreamTextArgs( { prompt, variables } );
121
+ const loadedPrompt = loadPrompt( prompt, variables );
122
+ const traceId = startTrace( 'streamText', { prompt, variables, loadedPrompt } );
123
+ const { model: modelId } = loadedPrompt.config;
124
+
125
+ try {
126
+ return AI.streamText( {
127
+ ...loadAiSdkOptionsFromPrompt( loadedPrompt ),
128
+ ...restOptions,
129
+ async onFinish( response ) {
130
+ const { text: result, totalUsage: usage, providerMetadata } = response;
131
+ const cost = await calculateLLMCallCost( { usage, modelId } );
132
+ emitEvent( 'llm:call_cost', { modelId, cost, usage } );
133
+ Tracing.addEventEnd( { id: traceId, details: { result, usage, cost, providerMetadata } } );
134
+ userOnFinish?.( response );
135
+ },
136
+ onError( event ) {
137
+ Tracing.addEventError( { id: traceId, details: event.error } );
138
+ userOnError?.( event );
139
+ }
140
+ } );
141
+ } catch ( error ) {
142
+ Tracing.addEventError( { id: traceId, details: error } );
143
+ throw error;
144
+ }
145
+ }