UNPKG

@maximai/maxim-js

Version:

Maxim AI JS SDK. Visit https://getmaxim.ai for more info.

462 lines 17.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MaximOpenAIResponses = void 0; const uuid_1 = require("uuid"); const utils_1 = require("./utils"); /** * Extract tool calls and their results from Responses API input. * Matches function_call items with corresponding function_call_output items. */ function extractToolCallsFromInput(input) { if (!input || !Array.isArray(input)) { return []; } const toolCalls = []; const toolCallMap = new Map(); // First pass: collect function_call items for (const item of input) { if (!item || typeof item !== "object") continue; if (item.type === "function_call" && item.call_id) { toolCallMap.set(item.call_id, { name: item.name || "unknown", arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments || {}), }); } } // Second pass: match function_call_output items with their calls for (const item of input) { if (!item || typeof item !== "object") continue; if (item.type === "function_call_output" && item.call_id) { const callInfo = toolCallMap.get(item.call_id); if (callInfo) { toolCalls.push({ id: item.call_id, name: callInfo.name, arguments: callInfo.arguments, result: typeof item.output === "string" ? item.output : JSON.stringify(item.output || ""), }); // Remove from map to avoid duplicates toolCallMap.delete(item.call_id); } } } // Add any remaining tool calls without results (pending calls) for (const [callId, callInfo] of toolCallMap) { toolCalls.push({ id: callId, name: callInfo.name, arguments: callInfo.arguments, }); } return toolCalls; } /** * Extract function calls from Responses API output. */ function extractToolCallsFromOutput(response) { const toolCalls = []; if (!response.output || !Array.isArray(response.output)) { return toolCalls; } for (const item of response.output) { if (!item || typeof item !== "object") continue; if (item.type === "function_call") { const funcCall = item; toolCalls.push({ id: funcCall.call_id || (0, uuid_1.v4)(), name: funcCall.name || "unknown", arguments: typeof funcCall.arguments === "string" ? funcCall.arguments : JSON.stringify(funcCall.arguments || {}), }); } } return toolCalls; } /** * Log extracted tool calls to a trace. */ function logToolCallsToTrace(trace, toolCalls) { for (const tc of toolCalls) { const toolCall = trace.toolCall({ id: tc.id, name: tc.name, description: `Tool call: ${tc.name}`, args: tc.arguments, }); if (tc.result !== undefined) { toolCall.result(tc.result); } } } /** * Async stream wrapper that logs to Maxim when the stream completes. * Wraps the OpenAI ResponseStream and captures the final response for logging. * Also tracks streaming metrics like time to first token and tokens per second. */ class ResponseStreamWrapper { constructor(streamManager, generation, trace, isLocalTrace) { this.streamManager = streamManager; this.generation = generation; this.trace = trace; this.isLocalTrace = isLocalTrace; this.capturedResponse = null; this.consumed = false; this.firstTokenTime = null; this.endTime = null; this.startTime = performance.now(); } async *[Symbol.asyncIterator]() { try { for await (const event of this.streamManager) { // Track time to first content token if (this.firstTokenTime === null && this.hasContent(event)) { this.firstTokenTime = performance.now(); } // Capture the response from the completed event if (event.type === "response.completed") { this.capturedResponse = event.response; } yield event; } } finally { this.endTime = performance.now(); if (!this.consumed) { this.consumed = true; // Use async finalization to await the finalResponse promise if needed this.finalizeLoggingAsync().catch((e) => { console.warn(`[MaximSDK][MaximOpenAIResponses] Error in async finalization: ${e}`); }); } } } /** * Check if the event has actual content (text delta). */ hasContent(event) { // Text delta events indicate actual content being generated if (event.type === "response.output_text.delta") { return true; } // Also consider content part added events if (event.type === "response.content_part.added") { return true; } return false; } /** * Get output tokens from the final response usage. */ getOutputTokens(response) { var _a; if (response === null || response === void 0 ? void 0 : response.usage) { return (_a = response.usage.output_tokens) !== null && _a !== void 0 ? _a : null; } return null; } async finalizeLoggingAsync() { var _a; const endTime = (_a = this.endTime) !== null && _a !== void 0 ? _a : performance.now(); try { // Try to get the final response - prefer captured from event, fallback to SDK method let finalResponse = this.capturedResponse; if (!finalResponse) { try { // The OpenAI SDK's ResponseStream.finalResponse() returns a Promise finalResponse = await this.streamManager.finalResponse(); } catch { // Best-effort - stream may have ended without a response } } if (finalResponse !== null) { try { if (this.generation) { this.generation.result(finalResponse); // Add streaming metrics if (this.firstTokenTime !== null) { const ttftMs = this.firstTokenTime - this.startTime; this.generation.addMetric("ttft_ms", ttftMs); const outputTokens = this.getOutputTokens(finalResponse); const generationDurationMs = endTime - this.firstTokenTime; if (generationDurationMs > 0 && outputTokens !== null && outputTokens > 0) { const tps = (outputTokens / generationDurationMs) * 1000; this.generation.addMetric("tokens_per_second", tps); } } } // Log tool calls from response output (new tool calls made by the model) if (this.trace) { const outputToolCalls = extractToolCallsFromOutput(finalResponse); logToolCallsToTrace(this.trace, outputToolCalls); try { const outputText = utils_1.OpenAIUtils.extractResponsesOutputText(finalResponse); if (typeof outputText === "string") { this.trace.output(outputText); } } catch { } } if (this.isLocalTrace && this.trace) { this.trace.end(); } } catch (e) { if (this.generation) { this.generation.error({ message: e instanceof Error ? e.message : String(e) }); } console.warn(`[MaximSDK][MaximOpenAIResponses] Error in logging streamed generation: ${e}`); } } else { // No final response available, still close the trace if local if (this.isLocalTrace && this.trace) { this.trace.end(); } } } catch (e) { if (this.generation) { this.generation.error({ message: e instanceof Error ? e.message : String(e) }); } console.warn(`[MaximSDK][MaximOpenAIResponses] Error in finalizing stream logging: ${e}`); } } } /** * Wrapped OpenAI Responses that automatically logs to Maxim. * * @example * ```typescript * const client = new MaximOpenAIClient(openai, logger); * * // Non-streaming * const response = await client.responses.create({ * model: "gpt-4.1", * input: "Hello, world!" * }); * * // Streaming * const stream = await client.responses.stream({ * model: "gpt-4.1", * input: "Tell me a story" * }); * for await (const event of stream) { * // process events * } * // Logging happens automatically when stream completes * ``` */ class MaximOpenAIResponses { constructor(client, logger) { this.client = client; this.logger = logger; } /** * Start a trace and generation for a Responses API call. */ startTraceAndGeneration(options) { const { extraHeaders, model, messages, modelParameters, isStreaming, input } = options; let traceId; let generationName; let sessionId; let traceTags; if (extraHeaders) { traceId = extraHeaders["maxim-trace-id"] || undefined; generationName = extraHeaders["maxim-generation-name"] || undefined; sessionId = extraHeaders["maxim-session-id"] || undefined; traceTags = extraHeaders["maxim-trace-tags"] || undefined; } const isLocalTrace = traceId === undefined; const finalTraceId = traceId || (0, uuid_1.v4)(); let trace = null; let generation = null; try { trace = this.logger.trace({ id: finalTraceId, sessionId }); // Add stream tag if streaming if (isStreaming) { trace.addTag("stream", "true"); } // Parse and add custom trace tags if (traceTags) { try { const parsedTags = JSON.parse(traceTags); for (const [key, value] of Object.entries(parsedTags)) { trace.addTag(key, value); } } catch (error) { console.warn(`[MaximSDK][MaximOpenAIResponses] Error in parsing trace tags: ${error}`); } } // Log tool calls from input (previous interactions) if (input) { const inputToolCalls = extractToolCallsFromInput(input); logToolCallsToTrace(trace, inputToolCalls); } generation = trace.generation({ id: (0, uuid_1.v4)(), model: String(model || "unknown"), provider: "openai", name: generationName, modelParameters, messages, }); } catch (e) { console.warn(`[MaximSDK][MaximOpenAIResponses] Error starting trace/generation: ${e}`); } return { isLocalTrace, trace, generation }; } /** * Creates a response with automatic Maxim logging. * * @example * ```typescript * const response = await client.responses.create({ * model: "gpt-4.1", * input: "What is the meaning of life?" * }); * ``` */ async create(params, options) { const extraHeaders = options === null || options === void 0 ? void 0 : options.headers; const model = params.model; const inputValue = params.input; const messages = utils_1.OpenAIUtils.parseResponsesInputToMessages(inputValue); const modelParameters = utils_1.OpenAIUtils.getResponsesModelParams(params); const { isLocalTrace, trace, generation } = this.startTraceAndGeneration({ extraHeaders, model, messages, modelParameters, isStreaming: false, input: inputValue, }); let response; try { response = (await this.client.responses.create(params, options)); } catch (e) { console.warn(`[MaximSDK][MaximOpenAIResponses] Error generating response: ${e}`); // Mark generation as errored if available if (generation) { generation.error({ message: e instanceof Error ? e.message : String(e), type: e instanceof Error ? e.constructor.name : undefined, }); } if (trace) { try { trace.error({ id: (0, uuid_1.v4)(), message: e instanceof Error ? e.message : String(e), type: e instanceof Error ? e.constructor.name : undefined, }); } catch { } } if (isLocalTrace && trace) { trace.end(); } throw e; } try { if (generation) { generation.result(response); } // Log tool calls from response output (new tool calls made by the model) if (trace) { const outputToolCalls = extractToolCallsFromOutput(response); logToolCallsToTrace(trace, outputToolCalls); try { const outputText = utils_1.OpenAIUtils.extractResponsesOutputText(response); if (typeof outputText === "string") { trace.output(outputText); } } catch { } } if (isLocalTrace && trace) { trace.end(); } } catch (e) { if (generation) { generation.error({ message: e instanceof Error ? e.message : String(e) }); } console.warn(`[MaximSDK][MaximOpenAIResponses] Error in logging generation: ${e}`); } return response; } /** * Creates a streaming response with automatic Maxim logging. * Logging happens automatically when the stream is consumed. * * @example * ```typescript * const stream = client.responses.stream({ * model: "gpt-4.1", * input: "Tell me a story" * }); * for await (const event of stream) { * if (event.type === 'response.output_text.delta') { * process.stdout.write(event.delta); * } * } * // Logging happens automatically when stream completes * ``` */ stream(params, options) { const extraHeaders = options === null || options === void 0 ? void 0 : options.headers; const model = params.model; const inputValue = params.input; const messages = utils_1.OpenAIUtils.parseResponsesInputToMessages(inputValue); const modelParameters = utils_1.OpenAIUtils.getResponsesModelParams(params); const { isLocalTrace, trace, generation } = this.startTraceAndGeneration({ extraHeaders, model, messages, modelParameters, isStreaming: true, input: inputValue, }); let streamManager; try { streamManager = this.client.responses.stream(params, options); } catch (e) { console.warn(`[MaximSDK][MaximOpenAIResponses] Error starting streaming response: ${e}`); // Mark generation as errored if available if (generation) { generation.error({ message: e instanceof Error ? e.message : String(e), type: e instanceof Error ? e.constructor.name : undefined, }); } // Ensure local trace is closed on error if (isLocalTrace && trace) { try { trace.error({ id: (0, uuid_1.v4)(), message: e instanceof Error ? e.message : String(e), type: e instanceof Error ? e.constructor.name : undefined, }); } catch { } try { trace.end(); } catch { } } throw e; } // Return a wrapper that delegates iteration to the underlying stream // but logs the final response automatically when iteration completes. return new ResponseStreamWrapper(streamManager, generation, trace, isLocalTrace); } } exports.MaximOpenAIResponses = MaximOpenAIResponses; //# sourceMappingURL=responses.js.map