@maximai/maxim-js
Version:
Maxim AI JS SDK. Visit https://getmaxim.ai for more info.
400 lines • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MaximAISDKWrapperV2 = void 0;
const utils_1 = require("./../utils");
const uuid_1 = require("uuid");
const utils_2 = require("./utils");
class MaximAISDKWrapperV2 {
/**
* @constructor
* Creates a new MaximAISDKWrapper instance.
*
* @param model - The Vercel AI SDK language model instance to wrap.
* @param logger - The MaximLogger instance to use for tracing and logging.
*/
constructor(model, logger) {
this.model = model;
this.logger = logger;
// Internal state to track trace across multiple doGenerate calls in a tool-call sequence
this.currentTraceId = null;
this.currentTrace = null;
this.currentSession = null;
this.isInToolCallSequence = false;
}
/**
* Sets up Maxim logging and tracing for a model call.
*
* Extracts Maxim metadata, parses prompt messages, and initializes session, trace, and span objects.
* Also logs user input to the trace if appropriate.
*
* @private
* @param options - The call options for the model invocation.
* @param promptMessages - The parsed prompt messages (used to detect tool calls).
* @returns An object containing maximMetadata, trace, session, span, and promptMessages.
*/
setupLogging(options, promptMessages) {
var _a, _b, _c, _d, _e, _f;
// Extracting the maxim object from `providerOptions`
const maximMetadata = (0, utils_1.extractMaximMetadataFromOptions)(options.providerOptions);
// Check if this is a continuation of a tool-call sequence (has tool results in prompt)
const hasToolResults = promptMessages.some((msg) => msg.role === "tool");
// Determine if we should reuse the existing trace or create a new one
const shouldReuseTrace = !(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId) && // User hasn't explicitly provided a traceId
this.isInToolCallSequence && // We're in a tool-call sequence
hasToolResults && // This call has tool results (continuation)
this.currentTraceId !== null; // We have an existing trace
let session = undefined;
let trace;
// If sessionId is passed, then create a session on Maxim. If not passed, do not create a session
if (maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.sessionId) {
// Reuse existing session if it's the same sessionId, otherwise create new
if (((_a = this.currentSession) === null || _a === void 0 ? void 0 : _a.id) === maximMetadata.sessionId) {
session = this.currentSession;
}
else {
session = this.logger.session({
id: maximMetadata.sessionId,
name: (_b = maximMetadata.sessionName) !== null && _b !== void 0 ? _b : "default-session",
tags: maximMetadata.sessionTags,
});
this.currentSession = session;
}
}
// Determine trace ID: use user-provided, reuse existing, or create new
const traceId = (_c = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId) !== null && _c !== void 0 ? _c : (shouldReuseTrace ? this.currentTraceId : (0, uuid_1.v4)());
const traceName = (_d = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceName) !== null && _d !== void 0 ? _d : "default-trace";
// Reuse existing trace if we're continuing a tool-call sequence
if (shouldReuseTrace && this.currentTrace) {
trace = this.currentTrace;
}
else {
// Create new trace
trace = session
? session.trace({
id: traceId,
name: traceName,
tags: maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceTags,
})
: this.logger.trace({
id: traceId,
name: traceName,
tags: maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceTags,
});
// Store trace state for reuse
this.currentTraceId = traceId;
this.currentTrace = trace;
}
// If this is the start of a new sequence (no tool results), reset the tool-call sequence flag
if (!hasToolResults && !(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId)) {
this.isInToolCallSequence = false;
}
const span = trace.span({
id: (_e = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.spanId) !== null && _e !== void 0 ? _e : (0, uuid_1.v4)(),
name: (_f = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.spanName) !== null && _f !== void 0 ? _f : "default-span",
tags: maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.spanTags,
});
const userMessage = promptMessages.findLast((msg) => msg.role === "user");
if (userMessage && userMessage.content) {
const userInput = userMessage.content;
if (typeof userInput === "string") {
trace.input(userInput);
}
else {
const userMessageContent = userInput[0];
switch (userMessageContent.type) {
case "text":
trace.input(userMessageContent.text);
break;
case "image_url":
trace.input(userMessageContent.image_url.url);
trace.addAttachment({
id: (0, uuid_1.v4)(),
type: "url",
url: userMessageContent.image_url.url,
});
break;
default:
break;
}
}
}
return { maximMetadata, trace, session, span, promptMessages };
}
/**
* Executes a text or object generation call with Maxim tracing and logging.
*
* This method is called internally by generateText and generateObject, and logs the generation
* result, errors, and relevant metadata to Maxim.
*
* @param options - The call options for the model invocation.
* @returns The result of the underlying model's doGenerate call.
*/
async doGenerate(options) {
var _a, _b, _c;
// Parse prompt messages first to detect tool calls
const promptMessages = (0, utils_2.parsePromptMessagesV2)(options.prompt);
const { maximMetadata, trace, span } = this.setupLogging(options, promptMessages);
let generation = undefined;
let response = undefined;
let hasToolCallsInResponse = false;
try {
generation = span.generation({
id: (0, uuid_1.v4)(),
name: (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationName) !== null && _a !== void 0 ? _a : "default-generation",
provider: (0, utils_1.determineProvider)(this.model.provider),
model: this.modelId,
messages: promptMessages,
modelParameters: (0, utils_1.extractModelParameters)(options),
tags: maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationTags,
});
(0, utils_2.processToolResultsFromPromptV2)(options.prompt, this.logger);
// Calling the original doGenerate function
response = await this.model.doGenerate(options);
// Check if response has tool calls - if so, we're in a tool-call sequence
hasToolCallsInResponse = (_c = (_b = response.content) === null || _b === void 0 ? void 0 : _b.some((content) => content.type === "tool-call")) !== null && _c !== void 0 ? _c : false;
if (hasToolCallsInResponse) {
// Mark that we're in a tool-call sequence
this.isInToolCallSequence = true;
}
// Create ToolCall entities for any tool calls in the response
if (response.content) {
for (const content of response.content) {
if (content.type === "tool-call") {
const toolCallId = content.toolCallId;
const toolName = content.toolName;
const toolInput = typeof content.input === "string" ? content.input : JSON.stringify(content.input);
span.toolCall({
id: toolCallId,
name: toolName,
description: toolName,
args: toolInput,
});
}
}
}
const res = (0, utils_2.convertDoGenerateResultToChatCompletionResultV2)(response);
generation.result(res);
generation.end();
return response;
}
catch (error) {
if (generation) {
generation.error({
message: error.message,
});
generation.end();
}
// Log error details
console.error("[MaximSDK] doGenerate failed:", error);
throw error;
}
finally {
span.end();
// End trace if:
// 1. User explicitly provided traceId (they manage it) - but don't reset state
// 2. OR response has no tool calls (sequence is complete or single call)
const shouldEndTrace = (maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId) || !hasToolCallsInResponse;
if (shouldEndTrace) {
// Reset state when ending trace (only if user didn't provide traceId)
if (!(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId)) {
this.currentTraceId = null;
this.currentTrace = null;
this.isInToolCallSequence = false;
trace.end();
}
}
await this.logger.flush();
}
}
/**
* Executes a streaming generation call with Maxim tracing and logging.
*
* This method is called internally by streamText and streamObject, and logs the streaming
* result, errors, and relevant metadata to Maxim.
*
* @param options - The call options for the model invocation.
* @returns The result of the underlying model's doStream call, with a wrapped stream.
*/
async doStream(options) {
var _a;
// Parse prompt messages first to detect tool calls
const promptMessages = (0, utils_2.parsePromptMessagesV2)(options.prompt);
const { maximMetadata, trace, span } = this.setupLogging(options, promptMessages);
let generation = undefined;
let hasToolCallsInResponse = false;
// Capture 'this' reference for use inside the stream
const wrapperInstance = this;
try {
// Process tool results from prompt before streaming (same as doGenerate)
// so toolCallResult/toolCallError are logged even if doStream or reader.read throws.
// Must be inside try so any throw flows into catch/flush path.
(0, utils_2.processToolResultsFromPromptV2)(options.prompt, this.logger);
// Calling the original doStream method
const startTime = performance.now();
const response = await this.model.doStream(options);
const modelProvider = (0, utils_1.determineProvider)(this.model.provider);
const modelId = this.modelId;
generation = span.generation({
id: (0, uuid_1.v4)(),
name: (_a = maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.generationName) !== null && _a !== void 0 ? _a : "default-generation",
provider: modelProvider,
model: modelId,
modelParameters: (0, utils_1.extractModelParameters)(options),
messages: promptMessages,
});
// going through the original stream to collect chunks and pass them without modifications to the stream
const chunks = [];
const firstToken = {
received: false,
time: null,
};
const stream = new ReadableStream({
async start(controller) {
try {
const reader = response.stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
// Stream is done, now process before closing
try {
// Check if we have tool calls in the stream
hasToolCallsInResponse = chunks.some((chunk) => chunk.type === "tool-call");
if (hasToolCallsInResponse) {
// Mark that we're in a tool-call sequence
wrapperInstance.isInToolCallSequence = true;
}
if (firstToken.received && firstToken.time) {
trace.addMetric("time_to_first_token (in ms)", firstToken.time - startTime);
firstToken.received = false;
firstToken.time = null;
}
const endTime = performance.now();
const textChunks = chunks.filter((chunk) => chunk.type === "text-delta" || chunk.type === "tool-input-delta");
trace.addMetric("tokens_per_second", textChunks.length / ((endTime - startTime) / 1000));
if (generation)
(0, utils_2.processStreamV2)(chunks, span, trace, generation, modelId, maximMetadata);
// Handle trace ending after stream processing
// End trace if:
// 1. User explicitly provided traceId (they manage it) - but don't reset state
// 2. OR response has no tool calls (sequence is complete or single call)
const shouldEndTrace = (maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId) || !hasToolCallsInResponse;
if (shouldEndTrace) {
// Reset state when ending trace (only if user didn't provide traceId)
if (!(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId)) {
wrapperInstance.currentTraceId = null;
wrapperInstance.currentTrace = null;
wrapperInstance.isInToolCallSequence = false;
trace.end();
}
}
// Flush logs after processStreamV2 and tool-result logging complete
await wrapperInstance.logger.flush();
}
catch (error) {
console.error("[MaximSDK] Processing failed:", error);
if (generation) {
generation.error({
message: error.message,
});
generation.end();
}
// Flush logs even on error
await wrapperInstance.logger.flush();
}
// Now close the stream
controller.close();
break;
}
if (!firstToken.received && (value.type === "text-delta" || value.type === "tool-input-delta")) {
firstToken.received = true;
firstToken.time = performance.now();
}
// Collect chunk and pass it through
chunks.push(value);
controller.enqueue(value);
}
}
catch (error) {
controller.error(error);
if (generation) {
generation.error({
message: error.message,
});
generation.end();
}
// Flush logs on stream error
await wrapperInstance.logger.flush();
}
},
});
// Return response with the logging stream - user gets real-time data without additional delay
return {
...response,
stream: stream,
};
}
catch (error) {
if (generation) {
generation.error({
message: error.message,
});
generation.end();
}
// Log error details
console.error("[MaximSDK] doStream failed:", error);
// Flush logs before throwing error
await this.logger.flush();
throw error;
}
finally {
// End trace if:
// 1. User explicitly provided traceId (they manage it) - but don't reset state
// 2. OR response has no tool calls (sequence is complete or single call)
const shouldEndTrace = (maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId) || !hasToolCallsInResponse;
if (shouldEndTrace) {
// Reset state when ending trace (only if user didn't provide traceId)
if (!(maximMetadata === null || maximMetadata === void 0 ? void 0 : maximMetadata.traceId)) {
this.currentTraceId = null;
this.currentTrace = null;
this.isInToolCallSequence = false;
trace.end();
}
}
// Note: flush() is now called after stream completion and in error paths, not here
}
}
/**
* Returns the model ID of the wrapped model.
*
* @returns The model ID.
*/
get modelId() {
return this.model.modelId;
}
/**
* Returns the provider name of the wrapped model.
*
* @returns The provider name.
*/
get provider() {
return this.model.provider;
}
/**
* Returns the specification version of the wrapped model.
*
* @returns The specification version.
*/
get specificationVersion() {
return this.model.specificationVersion;
}
/**
* Supported URL patterns by media type for the provider.
*
* @returns A map of supported URL patterns by media type (as a promise or a plain object).
*/
get supportedUrls() {
return this.model.supportedUrls;
}
}
exports.MaximAISDKWrapperV2 = MaximAISDKWrapperV2;
//# sourceMappingURL=wrapperV2.js.map