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

@company786/meridian-blue-sdk

v1.0.7

Published

Official TypeScript/JavaScript SDK for the Meridian-Blue API

Downloads

83

Readme

Meridian Blue SDK

Official TypeScript / JavaScript client for the Meridian Blue proxy API.

Meridian Blue is an OpenAI-compatible gateway that routes each request across multiple upstream providers (OpenAI, Anthropic, Groq, Gemini, Mistral, OpenRouter, Cloudflare Workers AI, and others), falls back automatically when a provider fails, and layers EU AI Act compliance (risk classification, purpose limitation, audit trail) on top.

This package is a thin, typed wrapper around the HTTP API. Anything the SDK documents is implemented on the server; everything not documented here is not yet supported.


Table of Contents


Install

npm install @company786/meridian-blue-sdk
# or
pnpm add @company786/meridian-blue-sdk
# or
yarn add @company786/meridian-blue-sdk

Requirements: Node.js 18+ and an ESM-capable toolchain. The package is distributed as ESM only ("type": "module").


Quick start

import { MeridianBlue } from "@company786/meridian-blue-sdk";

const client = new MeridianBlue({
  apiKey: process.env.MERIDIAN_BLUE_API_KEY!,
});

const response = await client.chat.completions({
  model: "claude-opus-4-7",
  messages: [{ role: "user", content: "Explain JWTs in one sentence." }],
});

console.log(response.choices[0].message.content);
console.log(`Cost: ${response.billing.cost} credits`);
console.log(`Balance: ${response.billing.balanceAfter}`);

Authentication

Create an API key in the dashboard (/dashboard/api-keys) and pass it to the constructor. Keys are prefixed with kp_live_.

const client = new MeridianBlue({
  apiKey: "kp_live_...",
  baseUrl: "https://api.meridianblue.ai", // optional — this is the default
  timeout: 30_000,                        // optional — default 30s
});

Configuration options:

| Field | Type | Default | Description | | --------- | ------ | ----------------------------- | ----------------------------- | | apiKey | string | — | Required. Your kp_live_ key | | baseUrl | string | https://api.meridianblue.ai | Override for self-hosted | | timeout | number | 30000 | HTTP timeout in ms |


Chat completions

POST /api/v1/chat/completions — OpenAI-compatible, with Meridian extensions.

Full parameter reference

| Field | Type | Required | Server behavior | | ------------------ | ------------- | -------- | --------------- | | model | string | one of | Single model id. Used by the router, stripped before upstream call. | | models | string[] | one of | Ordered fallback chain. Index 0 tried first. Stripped before upstream call. | | messages | ChatMessage[] | yes | Standard OpenAI messages array. Supports multimodal content. | | temperature | number | no | Forwarded to upstream. | | max_tokens | number | no | Forwarded to upstream. | | stream | boolean | no | Enables Server-Sent Events streaming. See Streaming. | | auto_truncate | boolean | no | If true, oversized prompts are trimmed server-side. Stripped before upstream call. | | purpose | string | no* | Declared purpose for EU AI Act risk classification. Stripped. | | user_consent_id | string | no* | Consent token. Required by server when purpose is high-risk. Stripped. | | end_user_id | string | no | Stable ID for the downstream user. Hashed + audited. Stripped. | | deployer_context | string | no | Deployment hint (e.g. internal_rag). Feeds the risk classifier. Stripped. | | any other field | — | no | Forwarded to upstream (e.g. top_p, presence_penalty, tools). |

* Required only when the server's policy layer classifies the request as high-risk. Low-risk requests can omit them.

"Stripped" means the server consumes the field internally and removes it before forwarding to the upstream provider, so providers that reject unknown top-level fields (Groq, most OpenAI-compat vendors) do not 400.

Exactly one of model or models must be provided.

Single model

const response = await client.chat.completions({
  model: "claude-opus-4-7",
  messages: [
    { role: "system", content: "You are a concise assistant." },
    { role: "user", content: "What is 2+2?" },
  ],
  temperature: 0.2,
  max_tokens: 100,
});

Model fallback chain (models[])

The proxy tries each model in order. If every provider for index 0 fails, it advances to index 1, and so on. The proxy never substitutes a model that isn't in this list.

const response = await client.chat.completions({
  models: [
    "openai/gpt-4o",
    "anthropic/claude-3-5-sonnet",
    "groq/llama-3.3-70b",
  ],
  messages: [{ role: "user", content: "Summarise the Rome Statute." }],
});

if (response.billing.isFallback) {
  console.log(`Served from fallback model: ${response.model}`);
}

Note: the free-tier daily quota applies to the entire chain. If a user is out of free requests, every entry in models must be paid-eligible and the user must have enough credit, or the request is rejected.

Multimodal content

messages[].content may be either a plain string or an array of content parts. Supported part types:

| Type | Shape | Notes | | ------------- | -------------------------------------------------------- | ----- | | text | { type: "text", text } | | | image_url | { type: "image_url", image_url: { url, detail? } } | URL or data:image/...;base64,... | | input_audio | { type: "input_audio", input_audio: { data, format } } | Base64 audio, format one of wav/mp3/flac/webm/ogg | | file | { type: "file", file: { url, mime_type } } | PDFs, documents | | video | { type: "video", video: { url, mime_type } } | Routed through native Gemini when required |

const response = await client.chat.completions({
  model: "openai/gpt-4o",
  messages: [
    {
      role: "user",
      content: [
        { type: "text", text: "What is in this image?" },
        {
          type: "image_url",
          image_url: { url: "https://example.com/cat.jpg", detail: "high" },
        },
      ],
    },
  ],
});

Multimodal requests (image/video) count against a separate per-user daily cap (5/day on the free tier) on top of the regular request counter.

Automatic truncation

If a prompt exceeds the selected model's context window, the default is to reject it. Setting auto_truncate: true makes the server apply a middle-out truncation strategy and reports what was removed.

const response = await client.chat.completions({
  model: "claude-opus-4-7",
  messages: veryLongChatHistory,
  auto_truncate: true,
});

if (response.truncation?.applied) {
  console.log(
    `Truncated ${response.truncation.messages_removed} messages ` +
    `(${response.truncation.original_tokens} → ${response.truncation.final_tokens} tokens)`,
  );
}

EU AI Act compliance fields

The server classifies every request against risk categories drawn from the EU AI Act. For high-risk categories (medical advice, legal advice, biometrics, social scoring, etc.) the server requires declared purpose and consent.

const response = await client.chat.completions({
  model: "openai/gpt-4o",
  messages: [
    { role: "user", content: "Given these symptoms, what could be wrong?" },
  ],
  purpose: "medical_advice_triage",
  user_consent_id: "consent_9f3b...",
  end_user_id: "user_42",
  deployer_context: "telehealth_pre_triage_chatbot",
});

console.log(response.risk_classification.level);        // "high"
console.log(response.risk_classification.triggered_rules);
console.log(response.user_notice?.text);                // user-facing disclosure
console.log(response.explainability.human_readable);

Purpose / consent are only enforced for high-risk categories — omitting them on a low-risk chat request is fine.

Streaming

stream: true produces a Server-Sent Events response (Content-Type: text/event-stream). The typed client.chat.completions() method does not parse SSE; use raw fetch or the underlying axios instance against /api/v1/chat/completions:

const res = await fetch("https://api.meridianblue.ai/api/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MERIDIAN_BLUE_API_KEY}`,
    "Content-Type": "application/json",
    Accept: "text/event-stream",
  },
  body: JSON.stringify({
    model: "claude-opus-4-7",
    messages: [{ role: "user", content: "Stream a haiku about JWTs." }],
    stream: true,
  }),
});

const reader = res.body!.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(decoder.decode(value));
}

Streaming responses still include billing and compliance metadata; they are emitted as trailing data: events after the final token.


Embeddings

POST /api/v1/embeddings

const response = await client.embeddings.create({
  model: "openai/text-embedding-3-small",
  input: ["hello world", "another sentence"],
});

console.log(response.data[0].embedding);   // number[]
console.log(response._meridian.provider);  // upstream provider used

| Field | Type | Required | Notes | | ----------------- | ------------------- | -------- | ----- | | model | string | yes | Embedding model id | | input | string | string[] | yes | Text(s) to embed | | encoding_format | string | no | e.g. "float", "base64" | | dimensions | number | no | Only for models that accept it |


Image generation

POST /api/v1/images/generations

const response = await client.images.generate({
  model: "openai/dall-e-3",
  prompt: "a neon-lit cyberpunk street at night, photorealistic",
  size: "1024x1024",
  quality: "hd",
  response_format: "url",
});

console.log(response.data[0].url);

| Field | Type | Required | Notes | | ----------------- | ------ | -------- | ----- | | model | string | yes | | | prompt | string | yes | | | n | number | no | Number of images | | size | string | no | e.g. "1024x1024", "1792x1024" | | quality | string | no | "standard" or "hd" | | style | string | no | "vivid" or "natural" | | response_format | string | no | "url" or "b64_json" |


Audio — text-to-speech

POST /api/v1/audio/speech — returns raw binary audio.

import { writeFileSync } from "node:fs";

const speech = await client.audio.speech({
  model: "openai/tts-1",
  input: "Good morning. The system is nominal.",
  voice: "nova",
  response_format: "mp3",
});

writeFileSync("out.mp3", Buffer.from(speech.data));
console.log(speech.provider, speech.latencyMs);

| Field | Type | Required | Notes | | ----------------- | ------ | -------- | ----- | | model | string | yes | | | input | string | yes | Text to synthesize | | voice | string | no | e.g. alloy, echo, fable, onyx, nova, shimmer | | response_format | string | no | mp3, opus, aac, flac, wav | | speed | number | no | 0.25 – 4.0 |


Audio — transcription

POST /api/v1/audio/transcriptions

import { readFileSync } from "node:fs";

const base64 = readFileSync("recording.mp3").toString("base64");

const transcription = await client.audio.transcribe({
  model: "openai/whisper-1",
  file: base64,
  language: "en",
});

console.log(transcription.text);

| Field | Type | Required | Notes | | ----------------- | ------ | -------- | ----- | | model | string | yes | | | file | string | yes | Base64-encoded audio content | | language | string | no | ISO 639-1 code | | response_format | string | no | json, text, srt, verbose_json, vtt | | temperature | number | no | |


Response shape

Every chat completion response includes both the standard OpenAI fields and the Meridian extensions:

{
  // Standard OpenAI fields
  id: "chatcmpl-...",
  object: "chat.completion",
  created: 1734567890,
  model: "claude-opus-4-7",
  choices: [{ index: 0, message: { role: "assistant", content: "..." }, finish_reason: "stop" }],
  usage: { prompt_tokens: 23, completion_tokens: 41, total_tokens: 64 },

  // Meridian extensions
  billing: {
    cost: 0.031,
    balanceAfter: 9974.203,
    isFallback: false,
    latencyMs: 842,
  },
  risk_classification: {
    level: "minimal",           // "minimal" | "limited" | "high" | "prohibited"
    reason: "general_query",
    triggered_rules: [],
    requires_human_review: false,
    auto_restricted: false,
    classifier_version: "1.0.0",
    confidence: 0.98,
  },
  explainability: {
    model_selection_reason: "Lowest-latency available provider meeting policy",
    risk_reasoning: "No high-risk keywords detected.",
    human_readable: "Routed to claude-opus-4-7 via OpenAI.",
  },
  user_notice: { /* only present for high-risk requests */ },
  truncation: { /* only present when auto_truncate applied */ },
}

Response headers

The proxy sets several informational headers on every response. Read them via the axios instance if you need them programmatically.

| Header | Description | | ----------------------------- | ----------- | | X-Meridian-Provider | Upstream provider that served the request | | X-Meridian-Model | Provider's native model id | | X-Meridian-Latency-Ms | Round-trip latency | | X-Meridian-Fallback | "true" if a fallback was used | | X-Meridian-Fallback-Count | Number of failed providers before success | | X-Meridian-Fallback-Chain | Comma-separated provider(status) list | | X-Meridian-Risk-Level | Risk classification level | | X-Meridian-Policy-Warnings | Set when deployer policy warnings fire | | X-Meridian-Override | Set when a manual routing override was applied | | X-Meridian-Review | "advisory_logged" or "blocking" | | X-Meridian-Cache | "hit" or "miss" for cacheable requests | | X-Meridian-Cache-Bypass | Reason the cache was skipped | | Retry-After | Seconds to wait (429 responses only) |


Error handling

The SDK maps HTTP errors to typed exceptions. Use instanceof checks.

import {
  MeridianBlue,
  AuthenticationError,
  InsufficientBalanceError,
  PermissionError,
  NotFoundError,
  RateLimitError,
  APIError,
  MeridianBlueError,
} from "@company786/meridian-blue-sdk";

try {
  await client.chat.completions({ /* ... */ });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${err.retryAfterSeconds}s.`);
  } else if (err instanceof InsufficientBalanceError) {
    console.log(`Balance: ${err.currentBalance}, debt limit: ${err.debtLimit}`);
  } else if (err instanceof AuthenticationError) {
    console.log("API key invalid or missing.");
  } else if (err instanceof PermissionError) {
    console.log("API key has been revoked.");
  } else if (err instanceof NotFoundError) {
    console.log("Model not found or not enabled for your account.");
  } else if (err instanceof APIError) {
    console.log("All upstream providers failed.");
  } else if (err instanceof MeridianBlueError) {
    console.log(`Network / unknown error: ${err.message}`);
  }
}

| Class | HTTP status | Trigger | | ---------------------------- | ----------- | ------- | | AuthenticationError | 401 | Missing or malformed API key | | InsufficientBalanceError | 402 | Balance below the debt cap and model is paid | | PermissionError | 403 | API key revoked, or free quota exhausted | | NotFoundError | 404 | Unknown model, or model not enabled for tenant | | RateLimitError | 429 | Per-key rate limit exceeded (carries retryAfterSeconds) | | APIError | 500 / 502 | All providers in the fallback chain failed | | MeridianBlueError | 0 | Network error / no HTTP response |


Examples in other languages

The Meridian Blue server speaks standard HTTP/JSON, so any language with an HTTP client can use it. The SDK is provided for TS/JS; other languages use raw HTTP.

Python (with requests)

import os
import requests

BASE_URL = "https://api.meridianblue.ai"
API_KEY = os.environ["MERIDIAN_BLUE_API_KEY"]

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# Single model
resp = requests.post(
    f"{BASE_URL}/api/v1/chat/completions",
    headers=headers,
    json={
        "model": "claude-opus-4-7",
        "messages": [{"role": "user", "content": "Hello"}],
    },
    timeout=30,
)
resp.raise_for_status()
data = resp.json()
print(data["choices"][0]["message"]["content"])
print("Cost:", data["billing"]["cost"], "credits")

# Fallback chain + compliance
resp = requests.post(
    f"{BASE_URL}/api/v1/chat/completions",
    headers=headers,
    json={
        "models": [
            "openai/gpt-4o",
            "anthropic/claude-3-5-sonnet",
            "groq/llama-3.3-70b",
        ],
        "messages": [{"role": "user", "content": "Summarise the Rome Statute."}],
        "auto_truncate": True,
        "purpose": "legal_research",
    },
    timeout=60,
)
data = resp.json()
print("Served by:", data["model"], "fallback?", data["billing"]["isFallback"])

Python (with the OpenAI SDK)

Meridian Blue is OpenAI-compatible — you can point the official OpenAI Python SDK at it. Meridian extensions (purpose, models, etc.) go through as extra_body; responses still contain the extensions but the OpenAI SDK just ignores unknown fields.

from openai import OpenAI

client = OpenAI(
    api_key=os.environ["MERIDIAN_BLUE_API_KEY"],
    base_url="https://api.meridianblue.ai/api/v1",
)

resp = client.chat.completions.create(
    model="claude-opus-4-7",
    messages=[{"role": "user", "content": "Hello"}],
    extra_body={
        "models": ["claude-opus-4-7", "groq/llama-3.3-70b"],
        "auto_truncate": True,
    },
)
print(resp.choices[0].message.content)
# Meridian fields are present on the raw response:
print(resp.model_extra.get("billing"))

cURL

# Single model
curl -sS https://api.meridianblue.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $MERIDIAN_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-4-7",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

# Fallback chain with compliance fields
curl -sS https://api.meridianblue.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $MERIDIAN_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "models": [
      "openai/gpt-4o",
      "anthropic/claude-3-5-sonnet",
      "groq/llama-3.3-70b"
    ],
    "messages": [{"role": "user", "content": "Summarise DORA."}],
    "purpose": "regulatory_research",
    "deployer_context": "internal_compliance_tool",
    "auto_truncate": true
  }'

# Streaming
curl -N https://api.meridianblue.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $MERIDIAN_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "model": "claude-opus-4-7",
    "messages": [{"role": "user", "content": "Stream a haiku."}],
    "stream": true
  }'

# Embeddings
curl -sS https://api.meridianblue.ai/api/v1/embeddings \
  -H "Authorization: Bearer $MERIDIAN_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/text-embedding-3-small",
    "input": ["hello world"]
  }'

# Image generation
curl -sS https://api.meridianblue.ai/api/v1/images/generations \
  -H "Authorization: Bearer $MERIDIAN_BLUE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/dall-e-3",
    "prompt": "a neon cyberpunk street",
    "size": "1024x1024"
  }'

Go (net/http)

package main

import (
    "bytes"
    "encoding/json"
    "io"
    "net/http"
    "os"
)

func main() {
    body, _ := json.Marshal(map[string]any{
        "model":    "claude-opus-4-7",
        "messages": []map[string]string{{"role": "user", "content": "Hello"}},
    })
    req, _ := http.NewRequest("POST",
        "https://api.meridianblue.ai/api/v1/chat/completions",
        bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("MERIDIAN_BLUE_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()
    out, _ := io.ReadAll(resp.Body)
    os.Stdout.Write(out)
}

Java (HttpClient)

var client = java.net.http.HttpClient.newHttpClient();
var body = """
  {"model":"claude-opus-4-7",
   "messages":[{"role":"user","content":"Hello"}]}
""";
var req = java.net.http.HttpRequest.newBuilder()
    .uri(java.net.URI.create("https://api.meridianblue.ai/api/v1/chat/completions"))
    .header("Authorization", "Bearer " + System.getenv("MERIDIAN_BLUE_API_KEY"))
    .header("Content-Type", "application/json")
    .POST(java.net.http.HttpRequest.BodyPublishers.ofString(body))
    .build();
var resp = client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(resp.body());

Publishing a new version

This package is published to npm under the scoped name @company786/meridian-blue-sdk. Scoped packages default to private on npm — the first publish must use --access public.

One-time setup

npm login                         # log in with an account that has
                                  # publish rights on the @company786 org
npm whoami                        # sanity check

Release flow

# 1. Make sure the working tree is clean and the build passes.
npm run build

# 2. Bump the version (choose one).
npm version patch                 # 1.0.0 -> 1.0.1
npm version minor                 # 1.0.0 -> 1.1.0
npm version major                 # 1.0.0 -> 2.0.0

# 3. Publish. `prepublishOnly` re-runs the build.
npm publish --access public

# 4. Push the tag that `npm version` created.
git push origin main --tags

Notes:

  • The files field in package.json restricts the tarball to dist/, so source .ts files are not shipped.
  • CI should run npm run build before publishing to catch type errors.
  • For a pre-release candidate, use npm version prerelease --preid=rc and publish with npm publish --tag next.

Verifying a release

# See what would be published without actually publishing.
npm pack --dry-run

# After publishing, install into a scratch project to verify.
mkdir /tmp/mb-check && cd /tmp/mb-check
npm init -y
npm install @company786/meridian-blue-sdk
node --input-type=module -e "
  import('@company786/meridian-blue-sdk').then(m => console.log(Object.keys(m)));
"

Testing against the live server

The default baseUrl (https://api.meridianblue.ai) points at the production deployment. A minimal smoke test:

export MERIDIAN_BLUE_API_KEY=kp_live_...
node --input-type=module -e "
  import { MeridianBlue } from '@company786/meridian-blue-sdk';
  const c = new MeridianBlue({ apiKey: process.env.MERIDIAN_BLUE_API_KEY });
  const r = await c.chat.completions({
    model: 'claude-opus-4-7',
    messages: [{ role: 'user', content: 'Say hi.' }],
  });
  console.log(r.choices[0].message.content);
  console.log('balance:', r.billing.balanceAfter);
"

If you get AuthenticationError, the key is wrong or has been revoked. If you get InsufficientBalanceError, top up credits in the dashboard. If you get NotFoundError, the model id is not enabled for your account — check the dashboard model catalogue.


License

MIT