@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
170 lines (169 loc) • 7.7 kB
JavaScript
/**
* Lifecycle Middleware
*
* Provides onFinish, onError, and onChunk callbacks for observing
* generation and streaming lifecycle events.
*
* This middleware is automatically enabled when lifecycle callbacks
* (onFinish, onError, onChunk) are passed in GenerateOptions or StreamOptions.
*/
import { logger } from "../../utils/logger.js";
import { isRecoverableError } from "../../utils/errorHandling.js";
import { fireOnErrorOnce } from "../../utils/lifecycleCallbacks.js";
/**
* Normalize a thrown value to an `Error` while preserving structured
* fields (`code`, `status`, `statusCode`, `retryAfter`, `details`,
* `cause`, etc.) that downstream retry logic and SDK callers depend on.
*
* Previously the lifecycle middleware did
* `error instanceof Error ? error : new Error(String(error))`, which
* silently dropped every custom property on non-Error throws — so a
* provider's `{ code: "RATE_LIMITED", retryAfter: 30 }` rejection
* surfaced to consumers as a bare `Error("[object Object]")`.
*/
function normalizeToError(error) {
if (error instanceof Error) {
return error;
}
if (typeof error === "object" && error !== null) {
const obj = error;
const msg = typeof obj.message === "string" ? obj.message : String(error);
// Object.assign keeps Error's prototype chain (including the
// captured stack from this synthetic Error's construction) while
// copying enumerable fields from the original throw.
return Object.assign(new Error(msg), obj);
}
return new Error(String(error));
}
export function createLifecycleMiddleware(config = {}) {
const metadata = {
id: "lifecycle",
name: "Lifecycle Callbacks",
description: "Provides onFinish, onError, and onChunk callbacks for generation and streaming lifecycle events",
priority: 110,
defaultEnabled: false,
};
const middleware = {
specificationVersion: "v3",
wrapGenerate: async ({ doGenerate, }) => {
const startTime = Date.now();
try {
const result = await doGenerate();
if (config.onFinish) {
try {
const content = result.content
?.map((c) => c.type === "text" ? c.text : "")
.join("") ?? "";
const callbackResult = config.onFinish({
text: content,
usage: result.usage
? {
promptTokens: result.usage.inputTokens?.total ?? 0,
completionTokens: result.usage.outputTokens?.total ?? 0,
}
: undefined,
duration: Date.now() - startTime,
finishReason: String(result.finishReason ?? ""),
});
Promise.resolve(callbackResult).catch((e) => {
logger.warn("[LifecycleMiddleware] onFinish callback error:", e);
});
}
catch (e) {
logger.warn("[LifecycleMiddleware] onFinish callback error:", e);
}
}
return result;
}
catch (error) {
const err = normalizeToError(error);
// fireOnErrorOnce stamps a Symbol on `err` so the SDK-level catch
// in neurolink.ts (and baseProvider.handleProviderError) skips
// its own onError fire for the same logical failure.
fireOnErrorOnce(config.onError, err, {
error: err,
duration: Date.now() - startTime,
recoverable: isRecoverableError(err),
});
// Rethrow the normalized err (not the raw `error`) so the
// fired-mark and any preserved structured fields propagate.
throw err;
}
},
wrapStream: async ({ doStream, }) => {
const startTime = Date.now();
try {
const result = await doStream();
if (!config.onChunk && !config.onFinish && !config.onError) {
return result;
}
let sequenceNumber = 0;
let accumulatedText = "";
const transformStream = new TransformStream({
transform(chunk, controller) {
try {
if (chunk.type === "text-delta") {
accumulatedText += chunk.textDelta;
}
if (config.onChunk && chunk.type) {
try {
const callbackResult = config.onChunk({
type: chunk.type,
textDelta: chunk.type === "text-delta" ? chunk.textDelta : undefined,
sequenceNumber: sequenceNumber++,
});
Promise.resolve(callbackResult).catch((e) => {
logger.warn("[LifecycleMiddleware] onChunk callback error:", e);
});
}
catch (e) {
logger.warn("[LifecycleMiddleware] onChunk callback error:", e);
}
}
controller.enqueue(chunk);
}
catch (error) {
const err = normalizeToError(error);
fireOnErrorOnce(config.onError, err, {
error: err,
duration: Date.now() - startTime,
recoverable: isRecoverableError(err),
});
throw err;
}
},
flush() {
if (config.onFinish) {
try {
const callbackResult = config.onFinish({
text: accumulatedText,
duration: Date.now() - startTime,
});
Promise.resolve(callbackResult).catch((e) => {
logger.warn("[LifecycleMiddleware] onFinish callback error:", e);
});
}
catch (e) {
logger.warn("[LifecycleMiddleware] onFinish callback error:", e);
}
}
},
});
return {
...result,
stream: result.stream.pipeThrough(transformStream),
};
}
catch (error) {
const err = normalizeToError(error);
fireOnErrorOnce(config.onError, err, {
error: err,
duration: Date.now() - startTime,
recoverable: isRecoverableError(err),
});
throw err;
}
},
};
return { ...middleware, metadata };
}