npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ybouhjira/hyperkit-llm-pipeline

v0.1.0

Published

Multi-LLM orchestration framework — define steps, chain models, observe results

Readme

@ybouhjira/hyperkit-llm-pipeline

Multi-LLM orchestration framework built with Effect-TS. Define typed pipeline steps, chain models together, and get full observability over every call — prompt, response, token usage, cost, and duration.

Part of the HyperKit monorepo. Used by @ybouhjira/hyperkit-ai-renderer to drive data-to-UI rendering pipelines.


Installation

npm install @ybouhjira/hyperkit-llm-pipeline

Peer dependency:

npm install effect@^3.0.0

Quick Start

import { Effect } from 'effect';
import { Schema as S } from 'effect';
import { defineStep, Pipeline, CliProvider } from '@ybouhjira/hyperkit-llm-pipeline';

// 1. Define the output schema for type-safe validation
const SummarySchema = S.Struct({ summary: S.String, bullets: S.Array(S.String) });

// 2. Define a step
const summarizeStep = defineStep({
  name: 'summarize',
  model: 'haiku',
  systemPrompt: 'You are a concise summarizer. Return valid JSON only.',
  outputSchema: SummarySchema,
  buildPrompt: (text: string) => `Summarize this text:\n\n${text}`,
  parseResponse: (response: string) => JSON.parse(response),
});

// 3. Build and run the pipeline
const pipeline = Pipeline.from('summarize-pipeline', summarizeStep);

const program = Pipeline.run(pipeline, 'Your long article text here...').pipe(
  Effect.provide(CliProvider)
);

const { output, trace } = await Effect.runPromise(program);
console.log(output.summary);
console.log(`Cost: $${trace.totalCostUsd.toFixed(6)}`);

Core Concepts

LlmProvider — the service interface

All LLM access goes through the LlmProvider Effect service. Steps declare a dependency on it; providers satisfy it at runtime via Effect.provide.

interface LlmRequest {
  readonly model: string;
  readonly systemPrompt: string;
  readonly userPrompt: string;
}

interface LlmResponse {
  readonly text: string;
  readonly tokens: { readonly input: number; readonly output: number };
  readonly costUsd: number;
}

class LlmProvider extends Context.Tag('LlmProvider')<
  LlmProvider,
  { readonly call: (request: LlmRequest) => Effect.Effect<LlmResponse, LlmError> }
>() {}

Steps

A step is the smallest unit of work: it receives typed input, builds a prompt, calls the LLM, parses the response, and validates the output against a schema.

interface StepConfig<In, Out> {
  name: string;
  model: string;
  systemPrompt: string;
  outputSchema: Schema<Out>;
  buildPrompt: (input: In) => string;
  parseResponse: (response: string) => unknown;
  retry?: {
    maxAttempts?: number; // default: 1 (no retry)
    feedbackOnError?: boolean; // append the parse/validation error to the next attempt's prompt
  };
}

Pipelines

A pipeline is an ordered sequence of steps where the output of step N becomes the input of step N+1. The type system enforces this chain at compile time.

// Single step
const p = Pipeline.from('my-pipeline', step);

// Chained steps
const p2 = Pipeline.pipe(p, nextStep);

// Run
const { output, trace } = await Effect.runPromise(
  Pipeline.run(p2, initialInput).pipe(Effect.provide(CliProvider))
);

API Reference

defineStep<In, Out>(config: StepConfig<In, Out>): Step<In, Out>

Creates a pipeline step. The generic types In and Out are inferred from buildPrompt and outputSchema.

Pipeline.from(name: string, step: Step<In, Out>): Pipeline<In, Out>

Creates a new pipeline from a single step.

Pipeline.pipe(pipeline: Pipeline<In, Mid>, step: Step<Mid, Out>): Pipeline<In, Out>

Appends a step to a pipeline. The output type of the pipeline must match the input type of the step.

Pipeline.run(pipeline, input): Effect<{ output, trace }, LlmError, LlmProvider>

Executes the pipeline. Returns an Effect that requires LlmProvider in its context. Provide it with Effect.provide(CliProvider) or a custom layer.

LlmProvider

Effect Context.Tag representing the LLM service. Implement it with any Layer<LlmProvider>.

LlmError

Tagged error class (_tag: 'LlmError') thrown when a provider call fails or all retry attempts are exhausted. Carries the model field.


Providers

CliProvider

The production provider. Routes calls to the Claude CLI or Gemini CLI based on the model name.

import { CliProvider } from '@ybouhjira/hyperkit-llm-pipeline';

const program = Pipeline.run(pipeline, input).pipe(Effect.provide(CliProvider));

Model routing:

  • model === 'gemini'gemini -p <prompt> (free, $0 cost)
  • anything else → claude --model <model> --print -p <prompt>

Supported Claude models and estimated pricing (per 1M tokens):

| Model | Input | Output | | -------- | ----- | ------ | | opus | $15 | $75 | | sonnet | $3 | $15 | | haiku | $0.25 | $1.25 | | gemini | $0 | $0 |

Unknown models fall back to haiku pricing.

Important: CliProvider uses synchronous execFileSync internally. It is designed for batch pipeline steps, not for interactive chat or streaming use cases.

Prerequisites: claude CLI and/or gemini CLI must be installed and authenticated in the system PATH.

MockProvider

Factory for deterministic test providers. Accepts either a response map or a callback function.

import { MockProvider } from '@ybouhjira/hyperkit-llm-pipeline';

// Map responses by model name
const provider = MockProvider({
  haiku: '{"summary": "short text", "bullets": ["point one"]}',
  sonnet: '{"result": "ok"}',
});

// Function form — inspect the full request
const provider = MockProvider((req: LlmRequest) => {
  if (req.model === 'haiku') return '{"value": 42}';
  return '{"answer": "default"}';
});

The function form receives the complete LlmRequest object (model, systemPrompt, userPrompt), making it suitable for testing prompt construction logic.

Custom Providers

Implement Layer<LlmProvider> to integrate any LLM backend:

import { Layer, Effect } from 'effect';
import { LlmProvider, LlmError } from '@ybouhjira/hyperkit-llm-pipeline';

const MyProvider = Layer.succeed(LlmProvider, {
  call: (request) =>
    Effect.tryPromise({
      try: async () => {
        const response = await myApiClient.complete(request);
        return {
          text: response.content,
          tokens: { input: response.usage.input, output: response.usage.output },
          costUsd: 0,
        };
      },
      catch: (err) => new LlmError(String(err), request.model),
    }),
});

Pipeline Composition Example

A two-step pipeline: extract structured data with a cheap model, then enrich it with a more capable one.

import { Effect } from 'effect';
import { Schema as S } from 'effect';
import { defineStep, Pipeline, CliProvider } from '@ybouhjira/hyperkit-llm-pipeline';

const ExtractedSchema = S.Struct({ topics: S.Array(S.String), sentiment: S.String });
const EnrichedSchema = S.Struct({
  topics: S.Array(S.String),
  sentiment: S.String,
  insight: S.String,
});

const extractStep = defineStep({
  name: 'extract',
  model: 'haiku',
  systemPrompt: 'Extract topics and sentiment as JSON. Return only JSON, no markdown.',
  outputSchema: ExtractedSchema,
  buildPrompt: (text: string) => `Analyze:\n\n${text}`,
  parseResponse: JSON.parse,
  retry: { maxAttempts: 3, feedbackOnError: true },
});

const enrichStep = defineStep({
  name: 'enrich',
  model: 'sonnet',
  systemPrompt: 'Add a one-sentence insight. Return only JSON, no markdown.',
  outputSchema: EnrichedSchema,
  buildPrompt: (extracted) => `Add insight to:\n\n${JSON.stringify(extracted)}`,
  parseResponse: JSON.parse,
});

const pipeline = Pipeline.pipe(Pipeline.from('analyze', extractStep), enrichStep);

const { output, trace } = await Effect.runPromise(
  Pipeline.run(pipeline, articleText).pipe(Effect.provide(CliProvider))
);

console.log(output.insight);
console.log(`Steps: ${trace.steps.length}, Total: $${trace.totalCostUsd.toFixed(6)}`);

Retry and Error Recovery

Configure retry on any step to handle transient parse errors or schema mismatches:

defineStep({
  // ...
  retry: {
    maxAttempts: 3, // Try up to 3 times before failing with LlmError
    feedbackOnError: true, // Append the error message to the next attempt's prompt
  },
});

When feedbackOnError is enabled, the second attempt's prompt becomes:

<original prompt>

Previous attempt failed with: Parse error: Unexpected token ...
Please fix the output.

This gives the LLM the chance to self-correct without requiring a separate validation step.


Observability and Tracing

Every Pipeline.run call returns a PipelineTrace alongside the output.

StepTrace

Each step in trace.steps contains:

| Field | Type | Description | | --------------- | --------- | ------------------------------------------------- | | id | string | Unique UUID for this execution | | stepName | string | Name from StepConfig | | model | string | Model used | | input | unknown | Input received by the step | | prompt | string | Final prompt sent to the LLM | | rawResponse | string | Raw text returned by the LLM | | output | unknown | Parsed and validated output | | startedAt | number | Unix timestamp (ms) | | completedAt | number | Unix timestamp (ms) | | durationMs | number | Wall-clock execution time | | tokens.input | number | Estimated input tokens | | tokens.output | number | Estimated output tokens | | costUsd | number | Estimated cost in USD | | attempts | number | Number of attempts made (1 = first try succeeded) | | error | string? | Error message if the step failed |

PipelineTrace

Aggregated totals for the entire pipeline run:

| Field | Type | Description | | ----------------- | -------------------------- | ------------------------------------------- | | pipelineName | string | Pipeline name | | steps | ReadonlyArray<StepTrace> | Per-step traces | | startedAt | number | Unix timestamp (ms) | | completedAt | number | Unix timestamp (ms) | | totalDurationMs | number | Total wall-clock time | | totalCostUsd | number | Sum of all step costs | | totalTokens | { input, output } | Sum of all step token usage | | success | boolean | true if all steps completed without error | | error | string? | Error message from the last failing step |


Testing

Use MockProvider to write fast, deterministic tests without any LLM calls:

import { Effect } from 'effect';
import { defineStep, Pipeline, MockProvider } from '@ybouhjira/hyperkit-llm-pipeline';

it('extracts the answer field', async () => {
  const step = defineStep({
    /* ... */
  });
  const program = Pipeline.run(Pipeline.from('test', step), 'input').pipe(
    Effect.provide(MockProvider({ haiku: '{"answer": "42"}' }))
  );

  const { output, trace } = await Effect.runPromise(program);
  expect(output.answer).toBe('42');
  expect(trace.success).toBe(true);
});

Roadmap

  • Streaming supportcall() currently returns a complete response. A stream() method is planned for interactive use cases.
  • OpenAI providerLayer<LlmProvider> wrapping the OpenAI API.
  • Ollama provider — Local model support via the Ollama REST API.
  • Cost estimation API — Pre-flight cost estimates based on prompt length and model pricing before making calls.
  • DAG pipelines — Parallel step execution for steps that do not depend on each other (the parentStepId field on StepTrace is reserved for this).

License

ISC