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

190proof

v1.0.127

Published

An opinionated unified interface for interacting with multiple AI providers including **OpenAI**, **Anthropic**, **Google**, **Groq**, **OpenRouter**, and **AWS Bedrock**. This package provides a consistent API for making requests to different LLM provide

Readme

190proof

An opinionated unified interface for interacting with multiple AI providers including OpenAI, Anthropic, Google, Groq, OpenRouter, and AWS Bedrock. This package provides a consistent API for making requests to different LLM providers while handling retries, streaming, and multimodal inputs.

Features

Fully-local unified interface across multiple AI providers that includes:

  • 🛠️ Consistent function/tool calling across all providers
  • 💬 Consistent message alternation & system instructions
  • 🖼️ Image format & size normalization
  • 🔄 Automatic retries with configurable attempts
  • 📡 Streaming by default
  • ☁️ Cloud service providers supported (Azure, AWS Bedrock)
  • 🔌 Provider prefix strings for any model without waiting for package updates

Installation

npm install 190proof

Usage

Basic Example

Use any model from any provider with the provider:model-id format:

import { callWithRetries, GenericPayload } from "190proof";

const payload: GenericPayload = {
  model: "openai:gpt-4o-mini",
  messages: [
    {
      role: "user",
      content: "Tell me a joke.",
    },
  ],
};

const response = await callWithRetries("my-request-id", payload);
console.log(response.content);

Using Different Providers

import { callWithRetries, GenericPayload } from "190proof";

// OpenAI
const openaiPayload: GenericPayload = {
  model: "openai:gpt-5",
  messages: [{ role: "user", content: "Hello!" }],
};

// Anthropic
const claudePayload: GenericPayload = {
  model: "anthropic:claude-sonnet-4-5",
  messages: [{ role: "user", content: "Hello!" }],
};

// Google
const geminiPayload: GenericPayload = {
  model: "google:gemini-2.0-flash",
  messages: [{ role: "user", content: "Hello!" }],
};

// Groq
const groqPayload: GenericPayload = {
  model: "groq:llama-3.3-70b-versatile",
  messages: [{ role: "user", content: "Hello!" }],
};

// OpenRouter
const openRouterPayload: GenericPayload = {
  model: "openrouter:google/gemma-4-31b-it:free",
  messages: [{ role: "user", content: "Hello!" }],
};

const response = await callWithRetries("request-id", claudePayload);

With Function Calling

const payload: GenericPayload = {
  model: "openai:gpt-4o",
  messages: [
    {
      role: "user",
      content: "What is the capital of France?",
    },
  ],
  functions: [
    {
      name: "get_country_capital",
      description: "Get the capital of a given country",
      parameters: {
        type: "object",
        properties: {
          country_name: {
            type: "string",
            description: "The name of the country",
          },
        },
        required: ["country_name"],
      },
    },
  ],
};

const response = await callWithRetries("function-call-example", payload);
// response.function_call contains { name: string, arguments: Record<string, any> }

With Images

const payload: GenericPayload = {
  model: "anthropic:claude-sonnet-4-5",
  messages: [
    {
      role: "user",
      content: "What's in this image?",
      files: [
        {
          mimeType: "image/jpeg",
          url: "https://example.com/image.jpg",
        },
      ],
    },
  ],
};

const response = await callWithRetries("image-example", payload);

How images reach the model depends on the provider. OpenAI, Anthropic, and Google get native image blocks. Groq is text-only: images degrade to an inline Image (url) text reference. OpenRouter sends OpenAI-style image_url content parts (the remote URL when present, else a data: URI) — but if the model has no vision-capable endpoints, OpenRouter rejects the request with a routing-layer 404, so the retry loop resends the payload with images degraded to the same inline text references Groq gets, and remembers the model (in-process, until restart) so later calls degrade up front. Messages without image attachments serialize identically either way.

With System Messages

const payload: GenericPayload = {
  model: "google:gemini-2.0-flash",
  messages: [
    {
      role: "system",
      content: "You are a helpful assistant that speaks in a friendly tone.",
    },
    {
      role: "user",
      content: "Tell me about yourself.",
    },
  ],
};

const response = await callWithRetries("system-message-example", payload);

Inspecting Model Routing

Use parseModelString to see how a model string will be routed:

import { parseModelString } from "190proof";

parseModelString("openai:gpt-7");
// → { provider: "openai", modelId: "gpt-7" }

parseModelString("openrouter:org/model-name:free");
// → { provider: "openrouter", modelId: "org/model-name:free" }

Provider Prefix Format

The model string format is provider:model-id, where the provider prefix is one of:

| Prefix | Provider | |---|---| | openai | OpenAI | | anthropic | Anthropic | | google | Google (Gemini) | | groq | Groq | | openrouter | OpenRouter |

The prefix is stripped before sending to the API, so the model ID should be exactly what the provider expects (e.g. "openai:gpt-4o" sends "gpt-4o" to OpenAI).

Supported Models

These models are tested. You can use any model with the provider:model-id format.

OpenAI

  • openai:gpt-5
  • openai:gpt-5-mini
  • openai:gpt-4.1
  • openai:gpt-4.1-mini
  • openai:gpt-4.1-nano
  • openai:gpt-4o
  • openai:gpt-4o-mini
  • openai:o3-mini
  • openai:o1-preview
  • openai:o1-mini

Anthropic

  • anthropic:claude-opus-4-5
  • anthropic:claude-sonnet-4-5
  • anthropic:claude-haiku-4-5
  • anthropic:claude-opus-4-1
  • anthropic:claude-opus-4-20250514
  • anthropic:claude-sonnet-4-20250514
  • anthropic:claude-3-5-sonnet-20241022
  • anthropic:claude-3-5-haiku-20241022

Google

  • google:gemini-3.1-flash-lite-preview
  • google:gemini-3-flash-preview
  • google:gemini-2.5-flash-preview-04-17
  • google:gemini-2.0-flash
  • google:gemini-2.0-flash-exp-image-generation
  • google:gemini-1.5-pro-latest

Groq

  • groq:llama-3.3-70b-versatile
  • groq:llama3-70b-8192
  • groq:qwen/qwen3-32b
  • groq:deepseek-r1-distill-llama-70b

OpenRouter

  • openrouter:google/gemma-4-31b-it:free
  • openrouter:google/gemma-4-31b-it

Environment Variables

Set the following environment variables for the providers you want to use:

# OpenAI
OPENAI_API_KEY=your-openai-api-key

# Anthropic
ANTHROPIC_API_KEY=your-anthropic-api-key

# Google
GEMINI_API_KEY=your-gemini-api-key

# Groq
GROQ_API_KEY=your-groq-api-key

# OpenRouter
OPENROUTER_API_KEY=your-openrouter-api-key

# AWS Bedrock (for Anthropic via Bedrock)
AWS_ACCESS_KEY_ID=your-aws-access-key
AWS_SECRET_ACCESS_KEY=your-aws-secret-key

API Reference

callWithRetries(identifier, payload, config?, retries?, chunkTimeoutMs?)

Main function to make requests to any supported AI provider.

Parameters

  • identifier: string | string[] - Unique identifier for the request (used for logging)
  • payload: GenericPayload - Request payload containing model, messages, and optional functions
  • config: OpenAIConfig | AnthropicAIConfig - Optional configuration for the specific provider
  • retries: number - Number of retry attempts (default: 5)
  • chunkTimeoutMs: number - Timeout for streaming chunks in ms (default: 15000)

Optional per-request knobs live on payload (GenericPayload):

  • payload.requestTimeoutMs: number - Per-attempt HTTP timeout in ms (default: 120000), honored by every adapter — except streaming OpenRouter attempts, which it deliberately does NOT bound (see below). For OpenRouter's non-streaming transport the default is 180000.
  • payload.streaming: boolean - OpenRouter-only (default: true). Streams the completion over SSE. A streaming attempt is bounded by two independent timers instead of requestTimeoutMs: streamTimeoutMs (total wall clock, default 600000) and the per-useful-chunk stall timeout (chunkTimeoutMs argument, default 15000). A chunk is "useful" only if it advances content, reasoning, tool-call fragments, finish_reason, or usage — SSE comment keep-alives (: OPENROUTER PROCESSING) and role-only deltas don't reset the stall timer, so a hung provider dies within one stall window while a healthy long generation can run to the total budget. Set streaming: false for the old single-JSON-body transport.
  • payload.streamTimeoutMs: number - OpenRouter-only: total wall-clock budget per streaming attempt (default: 600000).
  • payload.streamDeadlineAt: number - OpenRouter-only: absolute deadline (epoch ms) for the whole call including retries — the caller's turn budget. Each attempt gets min(streamTimeoutMs, deadline - now), and once under 10s remain the call fails fast instead of starting a generation that cannot be delivered. Use it whenever the caller has its own timeout: a per-attempt budget alone is re-granted on every retry and can outlive that timeout.
  • payload.thinkingConfig: Record<string, unknown> - Google-only: forwarded verbatim as generationConfig.thinkingConfig on the Gemini request — e.g. { thinkingBudget: 0 } to disable thinking, { thinkingLevel: "HIGH" } on models that take a level. Ignored by all other adapters; shapes are model-specific and validated by Google, not the SDK.
  • payload.reasoningEffort: string - OpenAI and OpenRouter reasoning effort. Valid values are model-dependent (none/minimal/low/medium/high/xhigh/max). OpenRouter: sent as the nested reasoning: { effort } object — the canonical form, and the only one that accepts max (the flat reasoning_effort field caps at xhigh). Direct OpenAI: sent flat as reasoning_effort; max is rejected there, and reasoning-by-default models (the gpt-5.6 family) reject function tools on /chat/completions with a 400 unless this is explicitly "none" — their implicit default is medium. Via OpenRouter the same models accept tools at any effort (OpenRouter fronts /v1/responses), so omitting this runs them at their native default. Ignored by all other adapters.

When a streaming attempt is cut at its total deadline and prose has already arrived, the partial answer is returned with truncated: true on the response rather than discarded — those tokens were generated and billed, so throwing them away costs money and gives the user nothing. Surface such a reply as incomplete. Salvage never applies to tool-call turns (half-streamed arguments are unparseable JSON), to stalls (the provider died mid-thought), or to caller aborts. When nothing is salvageable, the discard is logged with an approximate token count — aborted attempts never receive OpenRouter's usage chunk, so that log line is the only record of the wasted spend.

OpenRouter retries also perform moderation eviction: a provider content-moderation rejection (e.g. "Upstream error from Alibaba: Output data may contain inappropriate content.") is deterministic for a given payload, so on the first one the refusing provider is removed from the request's provider preferences (ignore += slug, order -= slug) and every remaining attempt reroutes to the next provider. Non-moderation errors retry with unchanged preferences, and fallbackModel still applies if the whole pool refuses.

  • payload.signal: AbortSignal - Caller-supplied cancellation. When it aborts, the in-flight provider request is cancelled and callWithRetries rejects immediately — it does not retry or fall back (both the retry loop and the fallback branch bail on signal.aborted). Threaded to the underlying fetch/axios/SDK call of each provider.

Returns

Promise<ParsedResponseMessage>:

interface ParsedResponseMessage {
  role: "assistant";
  content: string | null;
  function_call: FunctionCall | null;
  function_calls: FunctionCall[];
  files: File[]; // For models that return files (e.g., image generation)
  // Who actually served the response: OpenRouter's upstream provider from the
  // response body (e.g. "Baidu"), or the SDK provider name ("anthropic", ...)
  // for direct providers. On fallback, reflects the model that answered.
  provider?: string;
  usage: {
    prompt_tokens: number;
    completion_tokens: number;
    total_tokens: number;
    // Reasoning/thinking tokens spent before the visible answer; currently
    // populated from Google's usageMetadata.thoughtsTokenCount.
    thoughts_tokens?: number;
  } | null; // null when streaming
}

setLogger(sink | null)

190proof logs each call and retry to the console. Route those lines to your own logger, or pass null to silence them (useful when 190proof is a dependency of a library):

import { setLogger } from "190proof";

setLogger(null); // silent
setLogger({ log: myLog.info, warn: myLog.warn, error: myLog.error });

Errors thrown by callWithRetries never carry the HTTP request (and so never the API key); provider status and response body stay on error.response.

parseModelString(model)

Parses a model string into its provider and model ID components.

Parameters

  • model: string - A model string in "provider:model-id" format

Returns

{ provider: Provider, modelId: string }

Configuration Options

OpenAI Config

interface OpenAIConfig {
  service: "azure" | "openai";
  apiKey: string;
  /**
   * Optional base URL. Defaults to `https://api.openai.com/v1`. Set to point
   * at any OpenAI-compatible endpoint (e.g. a self-hosted proxy). The path
   * `/chat/completions` is appended automatically. Ignored for Azure.
   */
  baseUrl?: string;
  orgId?: string;
  modelConfigMap?: Record<
    string,
    {
      resource: string;
      deployment: string;
      apiVersion: string;
      apiKey: string;
      endpoint?: string;
    }
  >;
}

To talk to an OpenAI-compatible server instead of OpenAI itself:

await callWithRetries(
  "my-identifier",
  {
    model: "openai:gpt-4o-mini",
    messages: [{ role: "user", content: "hi" }],
  },
  {
    service: "openai",
    apiKey: process.env.SOME_SERVER_API_KEY,
    baseUrl: "https://your-proxy.example.com/v1",
  },
);

Anthropic Config

interface AnthropicAIConfig {
  service: "anthropic" | "bedrock";
}

License

ISC