@company786/meridian-blue-sdk
v1.0.7
Published
Official TypeScript/JavaScript SDK for the Meridian-Blue API
Downloads
83
Maintainers
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
- Quick start
- Authentication
- Chat completions
- Embeddings
- Image generation
- Audio — text-to-speech
- Audio — transcription
- Response shape
- Response headers
- Error handling
- Examples in other languages
- Publishing a new version
- Testing against the live server
Install
npm install @company786/meridian-blue-sdk
# or
pnpm add @company786/meridian-blue-sdk
# or
yarn add @company786/meridian-blue-sdkRequirements: 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 checkRelease 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 --tagsNotes:
- The
filesfield inpackage.jsonrestricts the tarball todist/, so source.tsfiles are not shipped. - CI should run
npm run buildbefore publishing to catch type errors. - For a pre-release candidate, use
npm version prerelease --preid=rcand publish withnpm 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
