Skip to content

Commit 7d594da

Browse files
authored
Merge branch 'main' into jb/sdk-1532/update-vercelai-example-with-provider
2 parents 0a8d763 + 28d3650 commit 7d594da

File tree

14 files changed

+453
-32
lines changed

14 files changed

+453
-32
lines changed

.release-please-manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"packages/sdk/cloudflare": "2.7.10",
99
"packages/sdk/combined-browser": "0.0.0",
1010
"packages/sdk/fastly": "0.2.1",
11-
"packages/sdk/react-native": "10.11.0",
11+
"packages/sdk/react-native": "10.12.0",
1212
"packages/sdk/server-ai": "0.12.3",
1313
"packages/sdk/server-node": "9.10.2",
1414
"packages/sdk/vercel": "1.3.34",
@@ -21,5 +21,5 @@
2121
"packages/store/node-server-sdk-redis": "4.2.14",
2222
"packages/telemetry/browser-telemetry": "1.0.11",
2323
"packages/telemetry/node-server-sdk-otel": "1.3.2",
24-
"packages/tooling/jest": "0.1.11"
24+
"packages/tooling/jest": "0.1.12"
2525
}

packages/ai-providers/server-ai-vercel/src/VercelProvider.ts

Lines changed: 219 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,53 @@ import type {
1010
LDTokenUsage,
1111
} from '@launchdarkly/server-sdk-ai';
1212

13+
import type {
14+
ModelUsageTokens,
15+
StreamResponse,
16+
TextResponse,
17+
VercelAIModelParameters,
18+
VercelAISDKConfig,
19+
VercelAISDKMapOptions,
20+
VercelAISDKProvider,
21+
} from './types';
22+
1323
/**
1424
* Vercel AI implementation of AIProvider.
1525
* This provider integrates Vercel AI SDK with LaunchDarkly's tracking capabilities.
1626
*/
1727
export class VercelProvider extends AIProvider {
1828
private _model: LanguageModel;
19-
private _parameters: Record<string, unknown>;
29+
private _parameters: VercelAIModelParameters;
2030

21-
constructor(model: LanguageModel, parameters: Record<string, unknown>, logger?: LDLogger) {
31+
/**
32+
* Constructor for the VercelProvider.
33+
* @param model - The Vercel AI model to use.
34+
* @param parameters - The Vercel AI model parameters.
35+
* @param logger - The logger to use for the Vercel AI provider.
36+
*/
37+
constructor(model: LanguageModel, parameters: VercelAIModelParameters, logger?: LDLogger) {
2238
super(logger);
2339
this._model = model;
2440
this._parameters = parameters;
2541
}
2642

2743
// =============================================================================
28-
// MAIN FACTORY METHOD
44+
// MAIN FACTORY METHODS
2945
// =============================================================================
3046

3147
/**
3248
* Static factory method to create a Vercel AIProvider from an AI configuration.
49+
* This method auto-detects the provider and creates the model.
50+
* Note: Messages from the AI config are not included in the provider - messages
51+
* should be passed at invocation time via invokeModel().
52+
*
53+
* @param aiConfig The LaunchDarkly AI configuration
54+
* @param logger Optional logger
55+
* @returns A Promise that resolves to a configured VercelProvider
3356
*/
3457
static async create(aiConfig: LDAIConfig, logger?: LDLogger): Promise<VercelProvider> {
3558
const model = await VercelProvider.createVercelModel(aiConfig);
36-
const parameters = aiConfig.model?.parameters || {};
59+
const parameters = VercelProvider.mapParameters(aiConfig.model?.parameters);
3760
return new VercelProvider(model, parameters, logger);
3861
}
3962

@@ -45,23 +68,18 @@ export class VercelProvider extends AIProvider {
4568
* Invoke the Vercel AI model with an array of messages.
4669
*/
4770
async invokeModel(messages: LDMessage[]): Promise<ChatResponse> {
48-
// Call Vercel AI generateText
49-
// Type assertion: our MinLanguageModel is compatible with the expected LanguageModel interface
50-
// The generateText function will work with any object that has the required properties
5171
const result = await generateText({
5272
model: this._model,
5373
messages,
5474
...this._parameters,
5575
});
5676

57-
// Create the assistant message
5877
const assistantMessage: LDMessage = {
5978
role: 'assistant',
6079
content: result.text,
6180
};
6281

63-
// Extract metrics including token usage and success status
64-
const metrics = VercelProvider.createAIMetrics(result);
82+
const metrics = VercelProvider.getAIMetricsFromResponse(result);
6583

6684
return {
6785
message: assistantMessage,
@@ -95,45 +113,220 @@ export class VercelProvider extends AIProvider {
95113
return mapping[lowercasedName] || lowercasedName;
96114
}
97115

116+
/**
117+
* Map Vercel AI SDK usage data to LaunchDarkly token usage.
118+
*
119+
* @param usageData Usage data from Vercel AI SDK
120+
* @returns LDTokenUsage
121+
*/
122+
static mapUsageDataToLDTokenUsage(usageData: ModelUsageTokens): LDTokenUsage {
123+
// Support v4 field names (promptTokens, completionTokens) for backward compatibility
124+
const { totalTokens, inputTokens, outputTokens, promptTokens, completionTokens } = usageData;
125+
return {
126+
total: totalTokens ?? 0,
127+
input: inputTokens ?? promptTokens ?? 0,
128+
output: outputTokens ?? completionTokens ?? 0,
129+
};
130+
}
131+
132+
/**
133+
* Get AI metrics from a Vercel AI SDK text response
134+
* This method extracts token usage information and success status from Vercel AI responses
135+
* and returns a LaunchDarkly AIMetrics object.
136+
* Supports both v4 and v5 field names for backward compatibility.
137+
*
138+
* @param response The response from generateText() or similar non-streaming operations
139+
* @returns LDAIMetrics with success status and token usage
140+
*
141+
* @example
142+
* const response = await aiConfig.tracker.trackMetricsOf(
143+
* VercelProvider.getAIMetricsFromResponse,
144+
* () => generateText(vercelConfig)
145+
* );
146+
*/
147+
static getAIMetricsFromResponse(response: TextResponse): LDAIMetrics {
148+
const finishReason = response?.finishReason ?? 'unknown';
149+
150+
// favor totalUsage over usage for cumulative usage across all steps
151+
let usage: LDTokenUsage | undefined;
152+
if (response?.totalUsage) {
153+
usage = VercelProvider.mapUsageDataToLDTokenUsage(response.totalUsage);
154+
} else if (response?.usage) {
155+
usage = VercelProvider.mapUsageDataToLDTokenUsage(response.usage);
156+
}
157+
158+
const success = finishReason !== 'error';
159+
160+
return {
161+
success,
162+
usage,
163+
};
164+
}
165+
98166
/**
99167
* Create AI metrics information from a Vercel AI response.
100168
* This method extracts token usage information and success status from Vercel AI responses
101169
* and returns a LaunchDarkly AIMetrics object.
102170
* Supports both v4 and v5 field names for backward compatibility.
171+
*
172+
* @deprecated Use `getAIMetricsFromResponse()` instead.
173+
* @param vercelResponse The response from generateText() or similar non-streaming operations
174+
* @returns LDAIMetrics with success status and token usage
175+
*/
176+
static createAIMetrics(vercelResponse: TextResponse): LDAIMetrics {
177+
return VercelProvider.getAIMetricsFromResponse(vercelResponse);
178+
}
179+
180+
/**
181+
* Get AI metrics from a Vercel AI SDK streaming result.
182+
*
183+
* This method waits for the stream to complete, then extracts metrics using totalUsage
184+
* (preferred for cumulative usage across all steps) or usage if totalUsage is unavailable.
185+
*
186+
* @param stream The stream result from streamText()
187+
* @returns A Promise that resolves to LDAIMetrics
188+
*
189+
* @example
190+
* const stream = aiConfig.tracker.trackStreamMetricsOf(
191+
* () => streamText(vercelConfig),
192+
* VercelProvider.getAIMetricsFromStream
193+
* );
103194
*/
104-
static createAIMetrics(vercelResponse: any): LDAIMetrics {
105-
// Extract token usage if available
195+
static async getAIMetricsFromStream(stream: StreamResponse): Promise<LDAIMetrics> {
196+
const finishReason = (await stream.finishReason?.catch(() => 'error')) ?? 'unknown';
197+
198+
// favor totalUsage over usage for cumulative usage across all steps
106199
let usage: LDTokenUsage | undefined;
107-
if (vercelResponse?.usage) {
108-
const { totalTokens, inputTokens, promptTokens, outputTokens, completionTokens } =
109-
vercelResponse.usage;
110-
usage = {
111-
total: totalTokens ?? 0,
112-
input: inputTokens ?? promptTokens ?? 0,
113-
output: outputTokens ?? completionTokens ?? 0,
114-
};
200+
if (stream.totalUsage) {
201+
const usageData = await stream.totalUsage;
202+
usage = VercelProvider.mapUsageDataToLDTokenUsage(usageData);
203+
} else if (stream.usage) {
204+
const usageData = await stream.usage;
205+
usage = VercelProvider.mapUsageDataToLDTokenUsage(usageData);
115206
}
116207

117-
// Vercel AI responses that complete successfully are considered successful
208+
const success = finishReason !== 'error';
209+
118210
return {
119-
success: true,
211+
success,
120212
usage,
121213
};
122214
}
123215

216+
/**
217+
* Map LaunchDarkly model parameters to Vercel AI SDK parameters.
218+
*
219+
* Parameter mappings:
220+
* - max_tokens → maxTokens
221+
* - max_completion_tokens → maxOutputTokens
222+
* - temperature → temperature
223+
* - top_p → topP
224+
* - top_k → topK
225+
* - presence_penalty → presencePenalty
226+
* - frequency_penalty → frequencyPenalty
227+
* - stop → stopSequences
228+
* - seed → seed
229+
*
230+
* @param parameters The LaunchDarkly model parameters to map
231+
* @returns An object containing mapped Vercel AI SDK parameters
232+
*/
233+
static mapParameters(parameters?: { [index: string]: unknown }): VercelAIModelParameters {
234+
if (!parameters) {
235+
return {};
236+
}
237+
238+
const params: VercelAIModelParameters = {};
239+
240+
if (parameters.max_tokens !== undefined) {
241+
params.maxTokens = parameters.max_tokens as number;
242+
}
243+
if (parameters.max_completion_tokens !== undefined) {
244+
params.maxOutputTokens = parameters.max_completion_tokens as number;
245+
}
246+
if (parameters.temperature !== undefined) {
247+
params.temperature = parameters.temperature as number;
248+
}
249+
if (parameters.top_p !== undefined) {
250+
params.topP = parameters.top_p as number;
251+
}
252+
if (parameters.top_k !== undefined) {
253+
params.topK = parameters.top_k as number;
254+
}
255+
if (parameters.presence_penalty !== undefined) {
256+
params.presencePenalty = parameters.presence_penalty as number;
257+
}
258+
if (parameters.frequency_penalty !== undefined) {
259+
params.frequencyPenalty = parameters.frequency_penalty as number;
260+
}
261+
if (parameters.stop !== undefined) {
262+
params.stopSequences = parameters.stop as string[];
263+
}
264+
if (parameters.seed !== undefined) {
265+
params.seed = parameters.seed as number;
266+
}
267+
268+
return params;
269+
}
270+
271+
/**
272+
* Convert an AI configuration to Vercel AI SDK parameters.
273+
* This static method allows converting an LDAIConfig to VercelAISDKConfig without
274+
* requiring an instance of VercelProvider.
275+
*
276+
* @param aiConfig The LaunchDarkly AI configuration
277+
* @param provider A Vercel AI SDK Provider or a map of provider names to Vercel AI SDK Providers
278+
* @param options Optional mapping options
279+
* @returns A configuration directly usable in Vercel AI SDK generateText() and streamText()
280+
* @throws {Error} if a Vercel AI SDK model cannot be determined from the given provider parameter
281+
*/
282+
static toVercelAISDK<TMod>(
283+
aiConfig: LDAIConfig,
284+
provider: VercelAISDKProvider<TMod> | Record<string, VercelAISDKProvider<TMod>>,
285+
options?: VercelAISDKMapOptions | undefined,
286+
): VercelAISDKConfig<TMod> {
287+
// Determine the model from the provider
288+
let model: TMod | undefined;
289+
if (typeof provider === 'function') {
290+
model = provider(aiConfig.model?.name ?? '');
291+
} else {
292+
model = provider[aiConfig.provider?.name ?? '']?.(aiConfig.model?.name ?? '');
293+
}
294+
if (!model) {
295+
throw new Error(
296+
'Vercel AI SDK model cannot be determined from the supplied provider parameter.',
297+
);
298+
}
299+
300+
// Merge messages from config and options
301+
let messages: LDMessage[] | undefined;
302+
const configMessages = ('messages' in aiConfig ? aiConfig.messages : undefined) as
303+
| LDMessage[]
304+
| undefined;
305+
if (configMessages || options?.nonInterpolatedMessages) {
306+
messages = [...(configMessages ?? []), ...(options?.nonInterpolatedMessages ?? [])];
307+
}
308+
309+
// Map parameters using the shared mapping method
310+
const params = VercelProvider.mapParameters(aiConfig.model?.parameters);
311+
312+
// Build and return the Vercel AI SDK configuration
313+
return {
314+
model,
315+
messages,
316+
...params,
317+
};
318+
}
319+
124320
/**
125321
* Create a Vercel AI model from an AI configuration.
126-
* This method creates a Vercel AI model based on the provider configuration.
322+
* This method auto-detects the provider and creates the model instance.
127323
*
128324
* @param aiConfig The LaunchDarkly AI configuration
129325
* @returns A Promise that resolves to a configured Vercel AI model
130326
*/
131327
static async createVercelModel(aiConfig: LDAIConfig): Promise<LanguageModel> {
132328
const providerName = VercelProvider.mapProvider(aiConfig.provider?.name || '');
133329
const modelName = aiConfig.model?.name || '';
134-
// Parameters are not used in model creation but kept for future use
135-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
136-
const parameters = aiConfig.model?.parameters || {};
137330

138331
// Map provider names to their corresponding Vercel AI SDK imports
139332
switch (providerName) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
export { VercelProvider } from './VercelProvider';
2+
export type {
3+
VercelAIModelParameters,
4+
VercelAISDKConfig,
5+
VercelAISDKMapOptions,
6+
VercelAISDKProvider,
7+
} from './types';

0 commit comments

Comments
 (0)