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

@openimport/ai

v0.2.0

Published

Bring-your-own-model AI column matching for openimport.

Readme

@openimport/ai

AI column matching for OpenImport, with bring-your-own-model. This package has no runtime dependencies and no hosted service. It turns the schema and columns into a prompt, hands that prompt to a generate function you write, and validates whatever comes back.

The model can do things the fuzzy matcher can't:

  • match by meaning across languages (Kontakt → email, Anrede → salutation)
  • read sample values (a column of @ addresses is an email column, whatever its header says)
  • propose transforms, such as splitting Full name into firstName / lastName or joining Street + No.

Suggestions go through the same pipeline as every other matcher. Users see them as suggestions in the mapping UI and can change them before importing.

pnpm add @openimport/ai

Usage

import { createImporter } from '@openimport/core'
import { csv } from '@openimport/csv'
import { aiMatching } from '@openimport/ai'

const importer = createImporter({
  schema,
  plugins: [
    csv(),
    aiMatching({
      // Call YOUR backend. Never put an API key in the browser.
      generate: async ({ system, prompt, signal }) => {
        const res = await fetch('/api/openimport/match', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({ system, prompt }),
          signal,
        })
        if (!res.ok) throw new Error(`AI matching failed: ${res.status}`)
        return res.text()
      },
      timeoutMs: 15_000,
    }),
  ],
})

generate is provider-agnostic: it takes { system, prompt, signal } and returns the model's raw text. Point it at any model (Claude, a local model, your gateway).

Server: Claude with the Anthropic SDK

A Next.js route handler (App Router). The same code works in Hono, Express or a worker: build the request the same way and return the text.

// app/api/openimport/match/route.ts
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic() // reads ANTHROPIC_API_KEY on the server

export async function POST(req: Request) {
  // Authenticate the caller here: an unauthenticated endpoint is an open LLM proxy.
  const { system, prompt } = (await req.json()) as { system: string; prompt: string }
  if (typeof system !== 'string' || typeof prompt !== 'string' || prompt.length > 50_000) {
    return new Response('Bad request', { status: 400 })
  }

  const response = await client.beta.messages.create(
    {
      model: 'claude-opus-5-5',
      max_tokens: 16000,
      // Column matching is a classification-style task: low effort is fast and cheap.
      output_config: { effort: 'low' },
      // If a safety classifier declines, retry on Anthropic's recommended fallback model.
      betas: ['server-side-fallback-2026-07-01'],
      fallbacks: 'default',
      system,
      messages: [{ role: 'user', content: prompt }],
    },
    { signal: req.signal },
  )

  // A declined request has no usable content. An empty answer makes OpenImport fall back to fuzzy matching.
  if (response.stop_reason === 'refusal') return new Response('{"matches":[]}')

  const text = response.content
    .filter((block) => block.type === 'text')
    .map((block) => block.text)
    .join('')
  return new Response(text, { headers: { 'content-type': 'application/json' } })
}

Privacy

By default the prompt contains up to 5 sample values per column, because they improve matching a lot. If your users' data must not reach a model, turn them off. The model then sees only headers, inferred types and your schema:

aiMatching({ generate, includeSamples: false })

Samples are truncated to 60 characters. The prompt never contains complete rows.

Options

| Option | Default | Description | | ---------------- | ------- | ------------------------------------------------------------------------------------------------- | | generate | — | ({ system, prompt, signal }) => Promise<string>. Required. | | priority | 10 | Matcher priority. The built-in fuzzy matcher has 0. On equal confidence the higher priority wins. | | minConfidence | 0.5 | Suggestions below this are dropped. | | maxSamples | 5 | Sample values per column sent to the model. | | includeSamples | true | Send sample values at all. | | transforms | true | Let the model propose transform pipelines (split, join, template, …). | | timeoutMs | — | Give up after this long and use the other matchers' suggestions. |

Suggestions at or above the importer's autoMapThreshold (default 0.7) are applied automatically. The others stay in state.suggestions.

Robustness

The model's answer is never trusted as is:

  • JSON is pulled out of ```json fences or surrounding prose. Bare arrays are accepted too.
  • Unknown target fields, unknown columns and already-mapped fields are dropped. Columns can be referenced by key or by label.
  • Confidence is clamped to 0…1, and duplicate targets keep the most confident entry.
  • Transform steps must use a built-in op (join, template, coalesce, split, trim, lowercase, uppercase, titlecase, replace, extract, prefix) with primitive params. Invalid steps are removed, and each removal halves the suggestion's confidence.
  • Errors, timeouts and garbage answers return no suggestions and log a console.warn. The import keeps working with fuzzy matching.

Lower-level API

import { aiMatcher, buildMatchPrompt, parseMatchResponse } from '@openimport/ai'

aiMatcher(options) // a Matcher, for when you compose plugins yourself
buildMatchPrompt(ctx, { includeSamples, maxSamples, transforms }) // → { system, prompt }
parseMatchResponse(text, ctx, { transforms, minConfidence }) // → MatchSuggestion[]

buildMatchPrompt is deterministic, and its system part never changes. That makes it easy to cache and to snapshot-test.