DEV Community

NeuroLink AI
NeuroLink AI

Posted on

Lifecycle Middleware: onFinish, onError, and onChunk Hooks

Lifecycle Middleware: onFinish, onError, and onChunk Hooks

You need visibility into AI request lifecycles without cluttering your business logic. NeuroLink's lifecycle middleware hooks provide exactly that—clean observation points for streaming chunks, completion events, and errors.

The Three Core Hooks

NeuroLink v9.30 introduces three callback mechanisms:

  • onChunk: Fires for every token in streaming responses
  • onFinish: Fires when generation completes successfully
  • onError: Fires when errors occur

Real-Time Token Processing with onChunk

Monitor streaming responses as they arrive:

import { NeuroLink } from "@juspay/neurolink";

const neurolink = new NeuroLink();

const result = await neurolink.stream({
  input: { text: "Write a story about a robot learning to paint" },
  middleware: [
    {
      name: "chunk-logger",
      onChunk: (chunk, { sequenceNumber }) => {
        // Track every token as it arrives
        analytics.track("token.received", {
          sequence: sequenceNumber,
          timestamp: Date.now(),
        });
      },
    },
  ],
});

for await (const chunk of result.stream) {
  process.stdout.write(chunk.content || "");
}
Enter fullscreen mode Exit fullscreen mode

The onChunk callback receives:

  • chunk: The actual chunk data (text delta, tool call, etc.)
  • sequenceNumber: Position in the stream (0, 1, 2...)
  • timestamp: When the chunk was received

Important: onChunk is fire-and-forget. If your callback returns a promise, errors are caught and logged—they don't break the stream.

Post-Generation Analytics with onFinish

Capture complete generation metrics:

const result = await neurolink.generate({
  input: { text: "Analyze this code for bugs" },
  middleware: [
    {
      name: "cost-tracker",
      onFinish: (result, metadata) => {
        const costEntry = {
          timestamp: Date.now(),
          provider: metadata.provider,
          model: metadata.model,
          tokens: result.usage,
          cost: calculateCost(result.usage, metadata.model),
          duration: metadata.duration,
        };

        costTracker.record(costEntry);

        // Alert on budget thresholds
        if (costTracker.total > ALERT_THRESHOLD) {
          notifyTeam("Cost threshold exceeded");
        }
      },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

onFinish receives:

  • result: The complete generation result (text, usage, finish reason)
  • metadata: Provider, model, timestamps, duration

Graceful Degradation with onError

Observe failures without suppressing them:

const result = await neurolink.generate({
  input: { text: "Process this transaction" },
  middleware: [
    {
      name: "error-handler",
      onError: (error, metadata) => {
        // Log for debugging
        logger.error("Generation failed", {
          error: error.message,
          provider: metadata.provider,
          model: metadata.model,
          recoverable: metadata.recoverable,
          duration: metadata.duration,
        });

        // Track in metrics
        metrics.increment("generation.errors", {
          provider: metadata.provider,
          type: error.name,
        });

        // Note: The original error is still thrown after this hook runs
      },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

The recoverable boolean indicates whether the error is transient (rate limit, timeout) or permanent (invalid API key, malformed request).

Complete Cost Tracking Example

Here's a production-ready cost tracking middleware:

interface CostRecord {
  timestamp: number;
  provider: string;
  model: string;
  promptTokens: number;
  completionTokens: number;
  cost: number;
  duration: number;
  success: boolean;
}

class CostTracker {
  private records: CostRecord[] = [];
  private totalCost = 0;
  private alertThreshold: number;
  private alertCallback?: (total: number, record: CostRecord) => void;

  constructor(
    alertThreshold = 100,
    alertCallback?: (total: number, record: CostRecord) => void
  ) {
    this.alertThreshold = alertThreshold;
    this.alertCallback = alertCallback;
  }

  record(entry: CostRecord): void {
    this.records.push(entry);
    this.totalCost += entry.cost;

    if (this.totalCost > this.alertThreshold && this.alertCallback) {
      this.alertCallback(this.totalCost, entry);
    }
  }

  getTotalCost(): number {
    return this.totalCost;
  }

  getRecords(): CostRecord[] {
    return [...this.records];
  }
}

// Pricing per 1K tokens
const PRICING: Record<string, { input: number; output: number }> = {
  "gpt-4o": { input: 0.005, output: 0.015 },
  "gpt-4o-mini": { input: 0.00015, output: 0.0006 },
  "claude-3-5-haiku": { input: 0.0008, output: 0.004 },
  "claude-3-5-sonnet": { input: 0.003, output: 0.015 },
};

function calculateCost(
  usage: { promptTokens: number; completionTokens: number },
  model: string
): number {
  const pricing = PRICING[model] || { input: 0.001, output: 0.002 };
  return (
    (usage.promptTokens / 1000) * pricing.input +
    (usage.completionTokens / 1000) * pricing.output
  );
}
Enter fullscreen mode Exit fullscreen mode

Middleware Composition

Lifecycle hooks integrate naturally with other NeuroLink middleware:

const neurolink = new NeuroLink({
  middleware: [
    // Priority 120: Request transformation
    { name: "transform", priority: 120 },
    // Priority 110: Lifecycle hooks (this article)
    { name: "lifecycle", priority: 110 },
    // Priority 100: Analytics
    { name: "analytics", priority: 100 },
    // Priority 90: Guardrails
    { name: "guardrails", priority: 90 },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Priority 110 ensures lifecycle middleware observes the full pipeline, including analytics overhead and guardrail processing.

Testing Lifecycle Hooks

Unit test without real API calls:

describe("CostTracker middleware", () => {
  it("tracks costs correctly", async () => {
    const tracker = new CostTracker();
    const mockNeurolink = createMockNeurolink();

    // Simulate successful generation
    mockNeurolink.simulateGenerate({
      text: "Test response",
      usage: { promptTokens: 100, completionTokens: 50 },
      provider: "openai",
      model: "gpt-4o-mini",
    });

    const result = await mockNeurolink.generate({
      input: { text: "Test" },
      middleware: [createCostMiddleware(tracker)],
    });

    expect(tracker.getTotalCost()).toBeGreaterThan(0);
    expect(tracker.getRecords()).toHaveLength(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Production Considerations

  1. Error propagation: Provider errors are re-thrown after onError fires
  2. Hook errors: Callback errors are swallowed and logged—they don't break your pipeline
  3. Async patterns: All hooks support async callbacks
  4. Conditional execution: Use middleware conditions to restrict hooks to specific providers or models

NeuroLink — The Universal AI SDK for TypeScript

Top comments (0)