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

@molecule/api-ai-openai

v1.4.4

Published

<!-- AUTO-GENERATED — DO NOT EDIT THIS FILE. Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json. Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates). To change this document, edit th

Readme

@molecule/api-ai-openai

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

OpenAI (GPT) AI provider for molecule.dev.

Quick Start

// npm install @molecule/api-ai-openai --workspace=api
import { setProvider, requireProvider } from '@molecule/api-ai'
import { provider } from '@molecule/api-ai-openai'

// Named registration — several AI providers can be bonded side by side
// (getProviderByName('openai') targets this one); the FIRST one
// registered also answers requireProvider() (see @molecule/api-ai).
setProvider('openai', provider) // reads OPENAI_API_KEY from the environment

let reply = ''
for await (const event of requireProvider().chat({
  messages: [{ role: 'user', content: 'Hello!' }],
})) {
  if (event.type === 'text') reply += event.content // or forward the chunk to the client (SSE)
}

Type

provider

Installation

npm install @molecule/api-ai-openai @molecule/api-ai @molecule/api-bond @molecule/api-secrets

API

Interfaces

OpenaiConfig

OpenAI provider configuration.

interface OpenaiConfig {
  /** Override the API key (defaults to `process.env.OPENAI_API_KEY`). */
  apiKey?: string
  /** Default model when callers don't specify one. */
  defaultModel?: string
  /** Default max output tokens. */
  maxTokens?: number
  /** Override the API base URL (for proxies / Azure). */
  baseUrl?: string
  /** Called on each rate-limited/overloaded upstream response, before any retry sleep. */
  onRateLimit?: AiRateLimitCallback
  /**
   * Which OpenAI endpoint to call. Defaults to `'responses'` (`/v1/responses`)
   * when the base URL is OpenAI's own API, and to `'chat-completions'`
   * (`/v1/chat/completions`) for any other base URL, since OpenAI-compatible
   * servers generally implement only chat/completions.
   */
  api?: OpenaiApi
}

ProcessEnv

Environment variables read by this provider.

interface ProcessEnv {
  /** OpenAI API key (required unless `config.apiKey` is passed). */
  OPENAI_API_KEY: string
  /** Base URL override (for proxies / Azure-compatible gateways). Defaults to `https://api.openai.com`. */
  OPENAI_BASE_URL?: string
}

Types

OpenaiApi

The OpenAI endpoint a provider instance calls.

type OpenaiApi = 'responses' | 'chat-completions'

Classes

OpenaiAIProvider

OpenAI Chat Completions provider implementing the AIProvider interface.

class OpenaiAIProvider implements AIProvider {
  readonly name = 'openai'
  private apiKey: string
  private defaultModel: string
  private maxTokens: number
  private baseUrl: string
  private onRateLimit?: AiRateLimitCallback
  private api: OpenaiApi

  constructor(config: OpenaiConfig = {}) {
    this.apiKey = config.apiKey ?? process.env.OPENAI_API_KEY ?? ''
    this.defaultModel = config.defaultModel ?? 'gpt-6-luna'
    this.maxTokens = config.maxTokens ?? 4096
    this.baseUrl = config.baseUrl ?? process.env.OPENAI_BASE_URL ?? 'https://api.openai.com'
    this.onRateLimit = config.onRateLimit
    this.api = config.api ?? defaultApiFor(this.baseUrl)

    // Fail fast with an actionable local error rather than a cryptic 401 on the
    // first request. The default `provider` export constructs lazily on first
    // use (see index.ts), so this surfaces the moment the provider is actually
    // used, not at bond/module-load time. A key is required only for the
    // official endpoint — a custom baseUrl may target a keyless self-hosted /
    // local server (Ollama, LM Studio), where the auth header is omitted below.
    const usingCustomEndpoint = Boolean(config.baseUrl ?? process.env.OPENAI_BASE_URL)
    if (!this.apiKey && !usingCustomEndpoint) {
      throw new Error(
        'OPENAI_API_KEY is not set. Add it to your environment to use the OpenAI AI provider.',
      )
    }
  }

  /**
   * Build the request body for `POST /v1/chat/completions`.
   *
   * @param params - The caller's chat parameters.
   * @param model - The resolved model id.
   * @param maxTokens - The resolved output-token limit.
   * @param useStream - Whether to request an SSE stream.
   * @returns The JSON request body.
   */
  private buildChatCompletionsBody(
    params: ChatParams,
    model: string,
    maxTokens: number,
    useStream: boolean,
  ): Record<string, unknown> {
    const messages = this.formatMessages(params.messages, params.system)

    const body: Record<string, unknown> = {
      // BYO / provider-native tunables (reasoning_effort, enable_thinking, …).
      // Spread FIRST so the structural fields below (model, messages,
      // max_completion_tokens, tools, stream) always win and cannot be broken.
      ...(params.extraBody ?? {}),
      // Abuse attribution (see ChatParams.endUserId). OpenAI DEPRECATED `user`
      // and replaced it with `safety_identifier` for exactly this purpose —
      // their guidance is a hashed, stable per-user string, which is what
      // endUserId already is. Sibling OpenAI-compatible bonds keep `user`: it is
      // the field they actually implement, verified accepted by deepseek, xai,
      // moonshot, alibaba and zhipu against their live APIs.
      //
      // This bond is also pointed at arbitrary BYO OpenAI-compatible endpoints
      // (molecule-dev's custom providers), so the unknown-field question matters
      // here: those same five providers all returned 200 when sent an unknown
      // `safety_identifier`, i.e. tolerating unknown body params is the norm.
      // A BYO endpoint therefore ignores this rather than rejecting it — and BYO
      // runs on the user's OWN key, where attribution protects nothing of ours.
      ...(params.endUserId ? { safety_identifier: params.endUserId } : {}),
      model,
      messages,
      // `max_completion_tokens`, NOT `max_tokens`. Every GPT-5-family model
      // hard-rejects the latter — "Unsupported parameter: 'max_tokens' is not
      // supported with this model" (400) — and the catalog's OpenAI entries are
      // all gpt-5.6-*, so this bond could not call any of its own models. The
      // newer name is accepted by the older generations too (verified 200 on
      // gpt-4o-mini and gpt-3.5-turbo), so there is no need to branch per model.
      max_completion_tokens: maxTokens,
    }
    if (params.temperature !== undefined) body.temperature = params.temperature

    // Reasoning depth: the caller resolves the model's native reasoning_effort
    // value (none | low | medium | high | xhigh on GPT-5.4/5.5-family) from the
    // model catalog and passes it as `thinking.effort`. Without it, omit the
    // param entirely (the model's own default applies — medium on gpt-5.5).
    if (params.thinking?.effort) {
      body.reasoning_effort = params.thinking.effort
    }

    const tools = params.tools?.length ? this.formatTools(params.tools) : null
    if (tools) {
      body.tools = tools
      if (params.toolChoice === 'required') {
        body.tool_choice = 'required'
      } else if (typeof params.toolChoice === 'object' && params.toolChoice?.type === 'tool') {
        body.tool_choice = { type: 'function', function: { name: params.toolChoice.name } }
      }
    }

    if (useStream) {
      body.stream = true
      body.stream_options = { include_usage: true }
    }
    return body
  }

  /**
   * Send a chat request and yield streamed `ChatEvent`s.
   *
   * @param params - Chat parameters.
   * @yields {ChatEvent}
   */
  async *chat(params: ChatParams): AsyncIterable<ChatEvent> {
    const model = params.model ?? this.defaultModel
    const maxTokens = params.maxTokens ?? this.maxTokens
    const useStream = params.stream !== false
    const isResponses = this.api === 'responses'

    const body = isResponses
      ? buildResponsesBody(params, model, maxTokens, useStream)
      : this.buildChatCompletionsBody(params, model, maxTokens, useStream)
    const path = isResponses ? '/v1/responses' : '/v1/chat/completions'

    // Per-call correlation (see ChatParams.requestId): a header rather than the
    // body's `user`, which is the stable per-account abuse-attribution value.
    const requestId = params.requestId ?? randomUUID()
    const call: OpenaiCallContext = {
      meta: { requestId, endpoint: `${this.baseUrl}${path}` },
      endUserId: params.endUserId,
    }

    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      // Omitted for a keyless custom endpoint (a self-hosted / local server).
      ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
      'X-Mol-Request-Id': requestId,
    }

    const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000
    const signal = params.signal ?? AbortSignal.timeout(DEFAULT_TIMEOUT_MS)

    const MAX_RETRIES = 3
    let response: Response | null = null
    for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
      response = await fetch(`${this.baseUrl}${path}`, {
        method: 'POST',
        headers,
        body: JSON.stringify(body),
        signal,
      })

      if (response.status === 429 || response.status === 503) {
        const willRetry = attempt < MAX_RETRIES
        // Retry-After is equally-valid as delta-seconds or an HTTP-date;
        // parseInt on the date form yields NaN, degrading the backoff to a
        // ~0ms retry against an already rate-limiting API. Guard it.
        const retryAfter = response.headers.get('retry-after')
        const parsedRetryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : NaN
        const hasRetryAfter =
          Number.isFinite(parsedRetryAfterSeconds) && parsedRetryAfterSeconds >= 0
        // Additive jitter (<500ms): concurrent callers rejected together must
        // not sleep identical delays and re-arrive as the same burst.
        const delayMs = willRetry
          ? (hasRetryAfter
              ? Math.min(parsedRetryAfterSeconds * 1000, 60_000)
              : Math.min(1000 * 2 ** attempt, 30_000)) + Math.floor(Math.random() * 500)
          : 0
        try {
          this.onRateLimit?.({
            provider: this.name,
            model,
            status: response.status,
            attempt: attempt + 1,
            willRetry,
            retryInMs: delayMs,
            ...(hasRetryAfter ? { retryAfterSeconds: parsedRetryAfterSeconds } : {}),
          })
        } catch (error) {
          // Host-app telemetry; its failure must never break the request.
// … truncated — the full declaration is in node_modules/@molecule/api-ai-openai/dist/provider.d.ts

Functions

createProvider(config)

Create an OpenAI AI provider instance.

function createProvider(config?: OpenaiConfig): AIProvider
  • config — OpenAI-specific configuration.

Returns: An AIProvider backed by OpenAI's Chat Completions API.

Constants

provider

The provider implementation.

const provider: AIProvider

aiOpenaiSecretDefinitions

Secret definitions required by the OpenAI AI bond.

const aiOpenaiSecretDefinitions: SecretDefinition[]

Core Interface

Implements @molecule/api-ai interface.

Bond Wiring

Setup function to register this provider with the bond system:

import { bond } from '@molecule/api-bond'
import { provider } from '@molecule/api-ai-openai'

export function setupAiOpenai(): void {
  bond('ai', 'openai', provider)
}

Injection Notes

Requirements

Peer dependencies:

  • @molecule/api-ai ^1.0.1
  • @molecule/api-bond ^1.0.1
  • @molecule/api-secrets ^1.0.1

Environment Variables

Runtime Dependencies

  • @molecule/api-ai
  • @molecule/api-bond
  • @molecule/api-secrets

Config: OPENAI_API_KEY (SERVER-side only) plus an optional default model id/base URL.

Endpoint: on OpenAI's own API (https://api.openai.com, the default) the provider calls the Responses API (/v1/responses); on any other baseUrl it calls /v1/chat/completions, the endpoint OpenAI-compatible servers (Ollama, LM Studio, gateways) implement. Override with api: 'responses' | 'chat-completions'. Current OpenAI reasoning models only accept function tools together with reasoning on /v1/responses (gpt-6-astra rejects every tool-carrying request on chat/completions), and only Responses carries OpenAI's server-side tools (serverTools, e.g. { type: 'web_search', name: 'web_search' }); chat/completions ignores serverTools. extraBody is merged into whichever endpoint's body is sent, so its keys must be that endpoint's params (e.g. reasoning, not reasoning_effort, on Responses).

Missing OPENAI_API_KEY fails fast: the provider throws naming the exact env var on first use (the exported provider is a lazy proxy, so this fires on the first chat() call, not at bond/module-load time) — it never silently sends an empty key.

Error message disambiguation: a plain 400 that ISN'T a context-length error (bad param, malformed tool schema) gets its own non-retryable message distinct from the generic "AI service error. Please try again." used for retryable failures.

E2E Tests

Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual chat/AI screens, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip. The sandbox HAS an AI provider bonded, so the flow runs live end-to-end; AI output is NON-DETERMINISTIC, so assert on STRUCTURE/behavior, not exact text:

  • [ ] A message sent through the real chat UI comes back as a RELEVANT AI reply — not an echo of the prompt, a hardcoded stub, or an empty bubble. Ask something with a checkable answer (e.g. "What is 2 + 2?") and confirm the response actually contains it ("4"), proving a live model answered.
  • [ ] If the app streams, tokens render INCREMENTALLY — text grows word by word in the UI, not one final blob dumped after a long frozen spinner. (A streamed chat() yields text chunks then a final done; a single late blob means the reply was awaited whole and streaming is broken.)
  • [ ] Multi-turn CONTEXT is preserved: a follow-up that refers back to the previous turn (e.g. after "2 + 2", ask "now double that" -> understood as 8) works — proving the full messages history is sent, not just the last line.
  • [ ] A provider failure (bad/missing key, rate limit, timeout) surfaces as a graceful in-UI error message, NOT a crash, blank screen, a spinner that never resolves, or an unhandled 500. Force one and watch the UI recover.
  • [ ] The provider key + the provider call are SERVER-side only: the key never reaches the browser (check the network tab, the JS bundle, and page globals), and no route proxies arbitrary prompts to the model without auth
    • a token cap — an open AI endpoint is an unbounded bill and abuse vector.