UNPKG

@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

193 lines 8.54 kB
/** * Curator P3-6: shared builder for the `NoOutputGeneratedError` sentinel * chunk. Each provider's stream-transformation generator catches the AI * SDK's `NoOutputGeneratedError` and yields this sentinel so downstream * telemetry has finish reason + token usage + provider error context * instead of just `{ noOutput: true, errorType: "..." }`. * * The AI SDK rejects `result.finishReason` / `result.totalUsage` in this * branch today (see `ai/src/generate-text/stream-text.ts` ~L1078); we * still attempt to await them so a future SDK version surfacing partial * values populates the sentinel automatically. When they reject we keep * conservative defaults (`finishReason: "error"`, zero usage). */ import { NoOutputGeneratedError } from "ai"; import { trace, context as otelContext } from "@opentelemetry/api"; export async function buildNoOutputSentinel(error, result, /** * Reviewer follow-up: AI SDK v6 wraps the AI SDK's * `NoOutputGeneratedError` without preserving the underlying provider * error in `error.cause`, and rejects `result.finishReason` / * `result.totalUsage` with the wrapped error too. To differentiate * content-filter / stop-sequence / provider-crash, providers can * capture the upstream error (e.g. via streamText's `onError` * callback) and pass it here. When provided, it takes precedence * over the AI SDK error for `providerError` and `modelResponseRaw`. */ underlyingError) { let finishReason = "error"; // Reviewer follow-up: include both AI SDK v4 (promptTokens / // completionTokens) and v6 (inputTokens / outputTokens) keys in the // default usage so downstream consumers reading either shape see // correct zeros instead of `undefined`. Also keep `totalTokens` for // back-compat. let usage = { promptTokens: 0, completionTokens: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, }; if (result) { try { if (result.finishReason !== undefined) { finishReason = await Promise.resolve(result.finishReason); } } catch { // Expected: AI SDK rejects with the same NoOutputGeneratedError. } try { if (result.totalUsage !== undefined) { usage = await Promise.resolve(result.totalUsage); } } catch { // Expected: AI SDK rejects with the same NoOutputGeneratedError. } } // Prefer the provider-captured underlying error for `providerError` / // `modelResponseRaw` since the AI SDK NoOutputGeneratedError doesn't // carry the actual upstream cause. Fall back to the AI SDK error. const messageSource = underlyingError instanceof Error ? underlyingError : underlyingError !== undefined ? new Error(String(underlyingError)) : error instanceof Error ? error : new Error(String(error)); const providerError = messageSource.message; const causeFromSource = messageSource.cause; // Reviewer follow-up: guard the `error.cause` access so it doesn't // throw a TypeError when `error` is null/undefined (only valid object // values can be indexed safely). const causeFromError = error !== null && typeof error === "object" ? error.cause : undefined; const cause = causeFromSource !== undefined ? causeFromSource : causeFromError; // Reviewer follow-up: always populate `modelResponseRaw` so downstream // telemetry consumers can rely on the field being a string. When neither // an `underlyingError` nor a `cause` is available, fall back to error // name + message so we still carry *something* about what the provider // returned. const modelResponseRaw = cause !== undefined ? String(cause).slice(0, 500) : `${messageSource.name}: ${messageSource.message}`.slice(0, 500); return { content: "", metadata: { noOutput: true, errorType: "NoOutputGeneratedError", finishReason, usage, providerError, modelResponseRaw, }, }; } /** * Curator P3-6 (round-2): the AI SDK v6 path that sets * `NoOutputGeneratedError` does NOT throw it from `result.textStream` * iteration — it sets the error as a *promise rejection* on * `result.finishReason` / `result.totalUsage` / `result.steps` (see * `ai/src/generate-text/stream-text.ts` ~L1078). Providers that only * catch errors thrown from `for await (chunk of result.textStream)` will * miss the production trigger entirely: the stream completes silently * with 0 chunks and the rejection bubbles as an unhandled rejection. * * This helper surfaces the rejection by awaiting `result.finishReason` * after the stream completes. Providers must call this AFTER iterating * the textStream when 0 chunks were yielded — the returned sentinel * (if non-null) carries the enriched metadata Curator's report needed. */ export async function detectPostStreamNoOutput(result, /** * Optional provider-captured underlying error (e.g. from streamText's * `onError` callback). When provided, the resulting sentinel will carry * the real provider error in `providerError` / `modelResponseRaw` * instead of the AI SDK's generic "No output generated" message. */ underlyingError) { if (result.finishReason === undefined) { return null; } try { await Promise.resolve(result.finishReason); // No rejection — the stream completed normally with a valid finish // reason; this is the empty-but-not-erroring case (e.g. AI SDK // recorded a step with no text), not the no-output failure. return null; } catch (err) { if (NoOutputGeneratedError.isInstance(err)) { return { sentinel: await buildNoOutputSentinel(err, result, underlyingError), error: err, }; } // Other rejection types (network errors, parse errors) are not the // bug-confirmed scenario — let the caller's existing error handling // surface them. return null; } } /** * Reviewer follow-up: every provider's post-stream NoOutput detect must * stamp the active OTel span so Pipeline B (`ContextEnricher.onEnd()` → * `applyNonErrorLangfuseLevel`) surfaces a WARNING-level Langfuse * observation with the enriched status message. Without this, only * `StreamHandler`-based providers produced the rich telemetry; the * provider-specific paths (openAI, openaiCompatible, litellm, * huggingFace, openRouter, anthropicBaseProvider) yielded the sentinel * to direct stream consumers but Pipeline B saw nothing. * * Stamps three attributes: * - `neurolink.no_output = true` (Pipeline B trigger) * - `langfuse.status_message` (enriched, with finishReason + tokens) * - `neurolink.no_output.finish_reason` (raw finish reason) * * Safe to call when tracing isn't initialized — silently no-ops. */ export function stampNoOutputSpan(sentinel) { try { const activeSpan = trace.getSpan(otelContext.active()); if (!activeSpan) { return; } activeSpan.setAttribute("neurolink.no_output", true); activeSpan.setAttribute("langfuse.status_message", buildNoOutputStatusMessage(sentinel.metadata.finishReason, sentinel.metadata.usage)); activeSpan.setAttribute("neurolink.no_output.finish_reason", String(sentinel.metadata.finishReason)); } catch { // Tracing not initialized — ignore. } } /** * Build the OTel `langfuse.status_message` summary string for a no-output * stream. Used by `StreamHandler.createTextStream` and any future provider * that wants to stamp the active span with the same enriched message. * * Reviewer follow-up: AI SDK v4 used `promptTokens` / `completionTokens`, * v6 uses `inputTokens` / `outputTokens`. Read both shapes so the message * is correct whichever version surfaced partial usage data. */ export function buildNoOutputStatusMessage(finishReason, usage) { const u = usage; const inputTokens = u?.inputTokens ?? u?.promptTokens ?? 0; const outputTokens = u?.outputTokens ?? u?.completionTokens ?? 0; return (`Stream produced no output (NoOutputGeneratedError): ` + `finishReason=${String(finishReason)}, ` + `inputTokens=${inputTokens}, ` + `outputTokens=${outputTokens}`); } //# sourceMappingURL=noOutputSentinel.js.map