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

openapi-ai-sdk

v0.7.0

Published

TypeScript SDK for openapi.ai — a drop-in extension of the OpenAI client, plus capability contracts: schema-frozen interfaces called like functions.

Readme

openapi-ai-sdk

npm License

The TypeScript SDK for OpenAPI — a drop-in replacement for the OpenAI client, plus the one thing no other gateway offers: capability contracts, versioned schema-frozen interfaces you call like functions while the platform guarantees what goes in and what comes out.

npm install openapi-ai-sdk

Drop-in for the OpenAI SDK

Change one import and one class name; everything else is your existing code.

import { OpenAPI } from "openapi-ai-sdk";   // was: import OpenAI from "openai"

const client = new OpenAPI({ apiKey: "gw_…" });

const response = await client.chat.completions.create({
  model: "openai/gpt-4.1-mini",
  messages: [{ role: "user", content: "hello" }],
});

OpenAPI extends OpenAI and overrides nothing. Streaming, tool calls, retries, and typed responses are the OpenAI SDK's own, and anything built on that client takes this one unchanged. What the platform adds appears as namespaces on the same client: capabilities, invocations, callbacks.

Capability contracts

The reason this SDK exists. Instead of integrating a provider's API, your organization defines a contract — a named, versioned pair of JSON Schemas — and providers register endpoints that fulfill it. The platform validates input before dispatch and output before you see it, retries across fulfillments, and never returns nonconforming data. Published versions are frozen forever: the schema you integrate against today cannot change under you.

const summarize = client.capabilities.get("doc-summary", { version: 3 });

// Synchronous — resolves with a conforming result, or throws a typed refusal:
const result = await summarize.invoke({ input: { text: "..." } });

// Asynchronous — a job handle now, the document when the work finishes:
const job = await summarize.submit({ input: { text: "..." } });
const output = (await job.wait()).output;

version is required on purpose: there is no floating "latest", so publishing v4 breaks nobody on v3.

From Zod schemas, typed end to end

Define the contract and the call site from one pair of schemas. register() is the mirror of get(): that one binds a version somebody already published, this one publishes the version and binds it. The schema pair you declare is the schema pair the platform enforces — one definition, not two hand-maintained copies:

import { z } from "zod";

const DocSummaryIn = z.object({ text: z.string(), maxWords: z.number().int().optional() });
const DocSummaryOut = z.object({ summary: z.string() });

const client = new OpenAPI({ apiKey: "gw_…", provisioningKey: "pk_…" });

// Publish the contract from the schemas — defines, drafts and freezes,
// then hands back a handle bound to what it just published.
const summarize = await client.capabilities.register("doc-summary", {
  input: DocSummaryIn,
  output: DocSummaryOut,
  description: "Summarize a document",   // capability policy, first definition only
});

// Or bind a version somebody already published.
const bound = client.capabilities.get("doc-summary", {
  version: 3,
  input: DocSummaryIn,
  output: DocSummaryOut,
});

const result = await bound.invoke({ input: { text: "long document" } });
result.output.summary;   // string — inferred, not `unknown` to fumble

Input is validated client-side before anything leaves the process — a refusal carries the same typed error shape as the gateway's own (code: "invalid_input", JSON-pointer paths) — and output is validated back through the bound schema. What you publish and what you type-check are one declaration, not two copies held together by discipline.

Anything implementing Standard Schema (Valibot, ArkType, …) works for typing and validation. JSON Schema derivation at register() time is built in for Zod 4 and for schemas exposing toJsonSchema() (ArkType); for anything else pass inputJsonSchema/outputJsonSchema alongside, or a plain JSON Schema pair as before — zod is an optional peer dependency, never required.

Registering is management, not inference, so it authenticates with a provisioning key (pk_…, contracts scope) — pass provisioningKey or set OPENAPI_PROVISIONING_KEY. The call is safe to re-run: an existing capability is not redefined, the next run publishes the next version. Publishing freezes both schemas forever; publish: false (or capabilities.draft(...)) stages a version without freezing it, and capabilities.publishVersion(name, n) freezes it later — after the review that permanence deserves.

Failures are typed, and kept deliberately distinct:

| What happened | How you see it | |---|---| | Refusal — nonconforming input, in-flight cap, exhausted fulfillments | CapabilityInvocationError at the call site, with the typed code, JSON-pointer paths, and attempt trail | | Failed invocation — terminal, already retried by the platform | Data: job.failed, job.error; reading job.output throws InvocationFailed so the happy path can't quietly read nothing | | wait() timeout | InvocationTimeout — changes nothing; the job keeps running server-side |

For long-running work, skip polling: pass callbackUrl to submit, then verify the signed webhook that arrives — client.callbacks.verify(rawBody, signatureHeader, { secret: "whsec_…" }) does the constant-time, tolerance-checked HMAC and returns the full document.

Files in, files out

A file slot is part of the contract like any other field — a string property carrying contentEncoding: "base64". fileSlot() writes the fragment for you, and File values handle both directions:

import { File, fileSlot } from "openapi-ai-sdk";

// In the published contract:
//   input_schema.properties.document = fileSlot("application/pdf")
//   output_schema.properties.redacted = fileSlot("application/pdf")

const redact = client.capabilities.get("doc-redact", { version: 1 });

const result = await redact.invoke({
  input: { document: File.fromPath("contract.pdf"), reason: "pii" },
});
(result.output.redacted as File).save("contract-redacted.pdf");

A plain Buffer or Uint8Array in the input encodes itself the same way, and output file slots on a bound handle come back as File objects with .bytes, .mediaType, and .save() — never base64 soup. The contract decides how the bytes travel, and the code does not change: inline slots ride the payload (10 MB cap by default); by-reference slots (format: "file-id") upload to the platform file store first and the payload carries only the id — outputs download back into the same File objects. The store itself is client.fileStore when you want an id without an invocation.

Contracts as tools, for any model

A published contract already is a tool definition:

const tools = [await client.capabilities.get("doc-summary", { version: 3 }).asTool()];
// … the model answers with tool_calls …
messages.push(await client.capabilities.executeToolCall(call));

executeToolCall runs the model's call through the platform and answers with the role:"tool" message. Model-caused failures — hallucinated arguments, nonconforming input — become tool-message content, never exceptions: an agent loop that crashes on a bad model turn is a loop nobody can run. The platform can also run the loop server-side, or serve your contracts over MCP — see invoking capabilities.

Static types, generated from the contract

npx openapi pull doc-summary@3

writes doc_summary_v3.ts: interfaces for both sides plus typed invoke/submit helpers, so the compiler enforces the same schema pair the provider is held to at runtime. Commit the file — it cannot rot, because published versions are frozen. Constructs an interface cannot express degrade to unknown, never to a crash. The key comes from --api-key or OPENAPI_API_KEY.

Fulfilling contracts (the provider side)

Your endpoint is your own server; the SDK ships the pieces every fulfiller needs:

import { verifyDispatch, parseDispatch, pushResult, pushFailure } from "openapi-ai-sdk";

verifyDispatch(req.headers.authorization, { secret: DISPATCH_SECRET });
const job = parseDispatch(req.body);                    // input already conforms
await pushResult(job.completionUrl!, { summary: "…" }); // or pushFailure(...)

A nonconforming result throws CompletionRejected with JSON-pointer paths — and the one-time completion token is burned (.gone marks a consumed or expired one). Pull-mode endpoints answer the dispatch with pullAck(jobId) and their registered status URL template (…/jobs/{job_id}/status, polled with the same bearer secret on an exponential backoff) with pullPending(), pullResult(output), or pullFailure(error) — the full wire contract is vendored in docs/fulfillment-protocol.md.

Rehearse before the platform judges you. Your first nonconforming output should fail a test, not burn a production attempt (npm install -D ajv — an optional peer, testing-time only):

import { ContractCheck, MockDispatcher } from "openapi-ai-sdk/testing";

const check = await ContractCheck.fromFile("contract.json");
const mock = new MockDispatcher(check, { secret: "fs_test" });

const dispatch = mock.dispatch({ input: { text: "hello" } });   // authorized, pre-validated
await myEndpoint(dispatch.body, dispatch.headers);              // your real handler
await pushResult(job.completionUrl!, output, { fetch: mock.fetch });
expect(mock.completed[0].output).toEqual(output);

The mock answers completions exactly as the platform does — 200 on conforming output, the 422 nonconforming_output envelope with paths, 410 once the token is spent — so your CompletionRejected handling is exercised too. check.assertOutput(result) is the one-line version, and npx openapi conform doc-summary@3 --output @sample.json (or --contract contract.json offline) is the same verdict from CI. Validation is pinned to JSON Schema Draft 2020-12, the draft the gateway itself validates with.

Run your inference through the credential the dispatch carries. When the contract sponsors platform inference, the job arrives with a gateway credential scoped to that attempt — the enterprise's requirements (allowed model providers, jurisdictions, retention) are enforced on it, its cost is theirs, and the work is attributable:

const client = gatewayClient(job);   // bound to this attempt's credential
await client.chat.completions.create({ model: "…", messages: [...] });

That is the whole integration. A contract without sponsorship dispatches no credential and gatewayClient throws NoGatewayCredential rather than handing you something that would be refused.

Routing, attribution, and usage

Everything below spreads into standard OpenAI calls:

| Helper | What it does | |---|---| | provider(...) | which provider serves the request: order, only, ignore, sort, jurisdictions, dataCollection | | models(...) | fallback models, tried in order | | transforms(...) | middle-out prompt compression | | metadata(...) | labels recorded with the request, filterable afterwards | | allowedModels(...) | narrow what this request may reach, within what the key permits | | routing(...) | merges the above into one spreadable body | | sessionId(...) | attributes this request to a conversation, right on the call | | withSession(...) | the same, for every request inside the callback |

import { provider, metadata, routing, sessionId } from "openapi-ai-sdk";

await client.chat.completions.create({
  model: "openai/gpt-4.1-mini",
  messages: [...],
  ...routing(
    provider({ order: ["groq"], sort: "throughput" }),
    metadata({ tenant: "acme" }),
    sessionId("conversation-42"),
  ),
});

jurisdictions: ["EU"] restricts to providers whose serving regions are all within those jurisdictions, and fails rather than falling back — being served elsewhere is the outcome you were ruling out. allowedModels is a convenience, not a control: the enforceable version is a key with its own allow-list. The default deployment is https://openapi.ai/v1; point baseUrl at your own stack to self-host.

The openapi CLI

Installed with the package — the same surface, from a shell:

npx openapi chat "say hello" --model openai/gpt-4.1-mini --session c42
npx openapi invoke doc-summary@3 --input '{"text": "..."}'
npx openapi invoke doc-redact@1 --file document=contract.pdf --save redacted=clean.pdf
npx openapi submit doc-summary@3 --input @input.json --wait
npx openapi invocation inv-… --wait
npx openapi contract doc-summary@3
npx openapi credits
npx openapi key
npx openapi publish doc-summary --input in.schema.json --output out.schema.json
npx openapi capabilities
npx openapi versions doc-summary
npx openapi pull doc-summary@3

JSON on stdout for machines, the assistant's words for chat, refusals on stderr with the typed code and paths. The key comes from OPENAPI_API_KEY (or --api-key), the base URL from OPENAPI_BASE_URL — except publish, capabilities, and versions, which authenticate with the provisioning key (OPENAPI_PROVISIONING_KEY / --provisioning-key) and need no inference credential at all; --draft stages a version without freezing it. In code, the same listings are client.capabilities.list() and client.capabilities.versions(name).

Documentation

Development

yarn install
yarn test
yarn typecheck && yarn lint
yarn build      # emits dist/ with declarations

test/vectors.json is generated from the Python SDK and checked by both suites: cacheKey must produce byte-identical output to openapi_ai._hash, so a store written by one SDK and read by the other cannot silently miss every entry.

License

Apache-2.0 — use it against the hosted platform or any self-hosted deployment.

Naming a contract across organizations

A capability name belongs to the organization that published it, not to the platform: two organizations may both have a doc-summary, against entirely different schemas. Every command accepts the qualified form when you want to be explicit about which one you mean:

npx openapi invoke acme/doc-summary@3 --input '{"text": "..."}'
npx openapi pull acme/doc-summary@3        # writes acme/doc_summary_v3.ts

The organization is checked, never used to look anything up — your API key decides which organization a call reaches. Naming a different one is refused before the request is sent, rather than quietly invoking your own contract of the same name. Unqualified references behave exactly as before, including where pull writes.