New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@evalguard/anthropic

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@evalguard/anthropic

Drop-in Anthropic SDK wrapper with EvalGuard guardrails, logging & cost tracking

Source
npmnpm
Version
1.3.0
Version published
Maintainers
1
Created
Source

@evalguard/anthropic

Drop-in Anthropic SDK wrapper that adds real-time guardrails, trace logging, and cost tracking via EvalGuard.

Installation

npm install @evalguard/anthropic @anthropic-ai/sdk

⚠️ ESM-only — requires "type": "module"

@evalguard/anthropic ships ES modules only ("type": "module", no CJS build). The quickstart below will not type-check or run in a default CommonJS TypeScript project — you get TS1479 ("the referenced file is an ECMAScript module and cannot be imported with require") and, because the peer SDK's types then resolve under a different module mode, a confusing TS2345 … Property '#private' … refers to a different member on the very first call.

To use this package, your consuming project must be ESM:

// package.json
{ "type": "module" }
// tsconfig.json
{ "compilerOptions": { "module": "node16", "moduleResolution": "node16" } }

Staying on CommonJS? A dynamic import() works from CJS — but it is not enough on its own. @anthropic-ai/sdk must be loaded dynamically too: a static import Anthropic from "@anthropic-ai/sdk" in a CJS file resolves the peer's types under CJS rules while the wrapper's resolve under ESM rules, so the two Anthropic classes are different types and the very first call still fails with TS2345 … Property '#private' is missing. Both imports must be dynamic:

…and both awaits must sit inside an async function. Top-level await is an ESM-only feature, so a bare await import(...) at file scope in a CJS module is TS1309: The current file is a CommonJS module and cannot use 'await' at the top level — which is why this block is an async function and not four loose statements:

// ✅ compiles under module/moduleResolution "node16", no "type": "module"
async function main() {
  const { wrapAnthropic } = await import("@evalguard/anthropic");
  const { default: Anthropic } = await import("@anthropic-ai/sdk");

  const anthropic = wrapAnthropic(new Anthropic(), {
    apiKey: "eg_...",
    projectId: "proj_...",
  });

  // …then use `anthropic` exactly as the quickstart below does.
  return anthropic;
}

void main();
// ❌ still fails — the peer import is static
import Anthropic from "@anthropic-ai/sdk";

async function main() {
  const { wrapAnthropic } = await import("@evalguard/anthropic");
  wrapAnthropic(new Anthropic(), { apiKey: "eg_..." });
}
// error TS2345: Property '#private' is missing in type 'Anthropic' but
//   required in type 'Anthropic'.

Every other snippet in this README uses the static import Anthropic from "@anthropic-ai/sdk" form for readability. Under CommonJS you must convert both lines, not just the @evalguard/anthropic one.

Node.js ≥ 22.12 can also require() an ESM module directly (require(esm)), but TypeScript still type-checks the import under CJS rules, so the dynamic-import form above is the supported path.

Quick Start

import Anthropic from "@anthropic-ai/sdk";
import { wrapAnthropic } from "@evalguard/anthropic";

const anthropic = wrapAnthropic(new Anthropic(), {
  apiKey: "eg_...",
  projectId: "proj_...",
});

// Use exactly like the normal Anthropic SDK — guardrails are automatic
const message = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, how are you?" }],
});

// `content` is an ordered array of blocks — text, thinking, tool_use, … — so
// narrow before reading `.text`. Indexing straight into `.content[0].text` is
// `error TS2339: Property 'text' does not exist on type 'ContentBlock'`.
const first = message.content[0];
if (first?.type === "text") {
  console.log(first.text);
}

Streaming

Streaming works transparently with both create({ stream: true }) and the stream() helper method.

Using create with stream: true

const stream = await anthropic.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Write a poem about AI safety" }],
  stream: true,
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

Using the stream() helper

const stream = await anthropic.messages.stream({
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Explain quantum computing" }],
});

for await (const event of stream) {
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
    process.stdout.write(event.delta.text);
  }
}

Configuration

const anthropic = wrapAnthropic(new Anthropic(), {
  // Required: your EvalGuard API key
  apiKey: "eg_...",

  // Optional: EvalGuard API base URL (default: https://evalguard.ai/api/v1)
  baseUrl: "https://your-evalguard-instance.com/api/v1",

  // Optional: block requests that fail guardrails (default: true)
  blockOnViolation: true,

  // Optional: log all requests to EvalGuard (default: true)
  enableLogging: true,

  // Optional: project ID for organizing traces
  projectId: "proj_...",

  // Optional: custom metadata attached to every trace
  metadata: { environment: "production", service: "chatbot" },

  // Optional: callback when a guardrail violation is detected
  onViolation: (result) => {
    console.warn("Guardrail violation:", result.violations);
  },
});

What It Does

PhaseAction
Pre-requestSends the prompt (including system prompt) to EvalGuard's firewall for prompt injection detection, PII scanning, and toxicity checks
LLM callPasses through to the real Anthropic API unchanged
Post-responseLogs model, tokens, latency, cost, and guardrail results as a trace to EvalGuard

Fail-Closed by Default

If EvalGuard is unreachable (network error, timeout, 5xx) and blockOnViolation is true (the default), the wrapper throws EvalGuardViolationError (type: "guardrail_unavailable") and does not call Anthropic — it fails closed, so an EvalGuard outage cannot silently bypass your guardrails.

To fail open instead (pass the request through to Anthropic on an EvalGuard outage), set blockOnViolation: false.

The legacy process-wide escape hatch still exists, but since 2026-08-03 it requires an explicit acknowledgement string — =1 no longer opens it:

EVALGUARD_GUARDRAIL_FAIL_OPEN_LEGACY=i-accept-unchecked-prompts-reaching-the-model

Any other value (including 1 and true) fails closed and prints a one-time rejection notice on stderr. Prefer blockOnViolation: false per integration over a process-wide hatch.

Error Handling

When blockOnViolation is true (default) and a guardrail check fails:

import { EvalGuardViolationError } from "@evalguard/anthropic";

try {
  const message = await anthropic.messages.create({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    messages: [{ role: "user", content: "malicious prompt..." }],
  });
} catch (error) {
  if (error instanceof EvalGuardViolationError) {
    console.log("Blocked:", error.violations);
    // [{ type: "prompt_injection", severity: "critical", message: "..." }]
  }
}

Set blockOnViolation: false to log violations without blocking:

const anthropic = wrapAnthropic(new Anthropic(), {
  apiKey: "eg_...",
  blockOnViolation: false,
  onViolation: (result) => {
    analytics.track("guardrail_violation", result);
  },
});

Cost estimates: unpriced models are reported as unpriced

estimateCost() used to invent a price for any model outside a ~60-row table: estimateCost("gpt-5", 1000, 500) and estimateCost("totally-unknown-model", 1000, 500) both returned 0.0105 from a blended $0.003/$0.015-per-1k fallback — with no flag and no warning. A FinOps figure you cannot tell apart from a real vendor price is worse than no figure at all.

Two things changed:

  • Coverage — 2,200+ model ids now resolve to a real, sourced rate (generated from EvalGuard's own pricing database, which is synced from the LiteLLM catalogue). gpt-5 is priced correctly.
  • Honesty — a genuinely unknown model is now visibly unknown.
import { estimateCostDetailed, isModelPriced } from "@evalguard/anthropic";

estimateCostDetailed("gpt-5", 1000, 500);
// { model: "gpt-5", costUsd: 0.00625, priced: true, pricingSource: "catalog" }

estimateCostDetailed("totally-unknown-model", 1000, 500);
// { model: "totally-unknown-model",
//   costUsd: null,              // <- never a fabricated number
//   priced: false,
//   pricingSource: "unpriced",
//   blendedFallbackUsd: 0.0105 } // <- opt-in rough figure, clearly labelled

isModelPriced("totally-unknown-model"); // false

estimateCost() still returns a number for backwards compatibility, but it now emits a one-time console.warn naming the unpriced model. Traces carry costPricingSource alongside cost, and cost is null for an unpriced model rather than a guess, so your EvalGuard dashboard shows "unpriced" instead of a fake dollar amount.

License

Apache-2.0

Keywords

anthropic

FAQs

Package last updated on 10 Sep 2026

Related posts