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

@mnemora/openai

v1.2.0

Published

EmbeddingProvider / LLMProvider の OpenAI 実装。zod スキーマを OpenAI の Structured Output (response_format: json_schema) へ翻訳する(docs/architecture.md §3.8)。

Readme

@mnemora/openai

EmbeddingProvider / LLMProvider の OpenAI 実装。zod スキーマを OpenAI の Structured Output(response_format: json_schema)へ翻訳する (docs/architecture.md §3.8)。

インストール

pnpm add @mnemora/openai @mnemora/core zod
# または
npm i @mnemora/openai @mnemora/core zod

下の例は completeStructured に渡すスキーマを zod で作るので、zod(@mnemora/core と同じメジャー、^4.5.4)も自分の依存として入れる(2026-09-27、pnpm pack した tarball を repo の外の空のプロジェクトに入れて確かめた。npm は依存の zod を hoist するので zod を足さなくても動くことがあるが、pnpm のような厳格な配置では Cannot find package 'zod' で止まる)。

前提

  • Node.js >= 22
  • ESM のみ("type": "module")。CommonJS からは Node 22.12 以降の require(esm) で読み込める(TypeScript は module/moduleResolution を nodenext にし、TypeScript 5.8 以降を使うこと。 5.7 以前の nodenext と、どの版の node16 も TS1479 になる。node10 は TypeScript 5.x なら パッケージの入口の型を解決できるが、exports を読まないので @mnemora/testkit/fixtures のような subpath は解決できず、TypeScript 6 で非推奨・7 で廃止された。2026-09-27 に TypeScript 5.0〜7.0 で実測)
  • OPENAI_API_KEY 環境変数(または apiKey オプション)が要る。無いと、呼び出す前に、new OpenAIEmbeddingProvider(...)・ new OpenAILLMProvider(...) の時点で OpenAI SDK が OpenAIError: Missing credentials. ... を投げる (OpenAILLMProviderError ではなく、kind も持たない)【実測 2026-09-27、pnpm pack した tarball を repo の外の空のプロジェクトに入れ、ネットワークを切って走らせた】
  • ⚠ 数値オプションは構築時に検査する(ADR 0498)。OpenAIEmbeddingProvider の dimensions は正の安全な整数、OpenAILLMProvider の temperature(渡すなら)は有限で 0 以上でなければ、TypeError(型が違う)か RangeError(数として不正)を投げる。temperature の上限は API ごとに違うので見ない。省略時の既定は変わらない。
  • 1つの OpenAIEmbeddingProvider インスタンスは1つの埋め込み空間(provider/model/dimensionsの組)に固定される。次元をモデルに応じて動的に変える使い方はできない

動く最小の例(型検査のみ確認・OPENAI_API_KEY が無いため未実行)

import { OpenAIEmbeddingProvider, OpenAILLMProvider } from "@mnemora/openai";
import { z } from "zod";

// apiKey を省略すると OPENAI_API_KEY 環境変数を読む。
const embeddingProvider = new OpenAIEmbeddingProvider({
  model: "text-embedding-3-small",
  dimensions: 1536,
});

const llmProvider = new OpenAILLMProvider({ model: "gpt-4o-mini" });

const ctx = { tenantId: "tenant-1" };

const [vector] = await embeddingProvider.embed(ctx, ["hello world"]);
console.log(vector?.length); // 1536

const response = await llmProvider.complete(ctx, {
  messages: [{ role: "user", content: "こんにちは" }],
});
console.log(response.content);

// zod スキーマを渡すと、OpenAI の Structured Output 経由で検証済みの値が返る
// (core・呼び出し側に OpenAI SDK の型は一切出てこない)。
const schema = z.object({ summary: z.string() });
const structured = await llmProvider.completeStructured(ctx, {
  prompt: { messages: [{ role: "user", content: "要約して" }] },
  schema,
});
console.log(structured.summary);

EmbeddingProvider.embed / LLMProvider.complete / LLMProvider.completeStructured の 契約(core 側の interface)は @mnemora/core を参照。 createRuntime() にそのまま渡して使う例は @mnemora/postgres の README にある。

⚠ 失敗は種類として返る(拒否を「空の成功」にしない)——ただし応答の形そのものが壊れている場合は別

OpenAI の拒否は HTTP 200 で返る。message.refusal に拒否理由の文字列が入り、 このとき message.content は null になる。SDK は例外を投げない。 LLMProvider.complete/completeStructured は content を読む前にこれを見て、 OpenAILLMProviderError を kind: "refusal" | "truncated" | "no_content" | "schema_unsupported" として 投げる(src/errors.ts 参照。@mnemora/anthropic の kind タクソノミーと対になる形)。

⚠ 2026-09-26 追記(Issue #885): kind が表すのは、この種類(OpenAILLMFailureKind)のどれかである。 HTTP 200 の応答オブジェクトそのものの形が壊れている場合——chat.completions.create の choices や embeddings.create の data がトップレベルからキーごと丸ごと無い場合 ({} が返る等)——は、kind の外にある生の例外(TypeError 等。壊れた JSON の SyntaxError・スキーマ不適合の ZodError と同じ扱い)がそのまま伝播する。 OpenAILLMProviderError にはならず、instanceof でも kind でも捕まえられない (埋め込み側の OpenAIEmbeddingProvider.embed はそもそも専用のエラー型を持たず、 壊れた応答は最初から生の例外がそのまま伝播する)。実 API がこの形を実際に返すかは 確認していない(詳細は ADR 0072 の同日付追記)。

⚠ 2026-09-30 追記(Issue #860): embed() は応答を検査する(下の 2026-09-26 の節は古くなった)

OpenAIEmbeddingProvider.embed は、応答が次のどれかを満たさなければ、素の Error(メッセージは OpenAIEmbeddingProvider: で始まる。専用のエラー型・kind は無い)を投げる: (1) response.data の件数が 入力の件数と等しい、(2) index が 0..n-1 をちょうど1回ずつ、(3) 各ベクトルの長さが dimensions と等しい、 (4) 成分がすべて有限(NaN/Infinity が無い)。メッセージには期待値・実際の値・何番目かを入れ、入力テキストの 本文と API キーは入れない。新しく例外になる場合が増える変更で、CHANGELOG.md の [1.2.0] に破壊的変更として書いた(response.data キー自体が無い応答は従来どおり生の TypeError)。 入力の上限超過は今もサーバの拒否に依存している(ADR 0305)。 実 API がこれらの食い違いを実際に返すかは確認していない(偽の fetch を本物の SDK に渡して確かめた)。

⚠ 2026-09-26 追記(Issue #860): embed() は応答の件数を確かめない(⚠ 2026-09-30 に古くなった。当時の記述として残す)

EmbeddingProvider.embed の契約は「入力と同じ件数・同じ順序でベクトルを返す」ことだが、 OpenAIEmbeddingProvider.embed はこれを実行時に確かめない。response.data を index で 並べ替えて返すだけで、件数が texts.length と食い違っていないかは検査しない ——@mnemora/local-embedding の LocalEmbeddingProvider.embed は件数・次元の食い違いを 検査して例外を投げるが、こちらは OpenAI のサーバが正しい件数を返すことに依存している (上限超過を「サーバの拒否に依存する」のと同じ形、ADR 0305)。 応答の件数が食い違ったときの戻り値は未定義である。packages/core の本番経路は常に 1件ずつ渡すため、この食い違いは踏まれていない。

⚠ 2026-09-26 追記(Issue #884): client を省略すると SDK 既定の再試行・timeout が効く

client を省略した OpenAILLMProvider/OpenAIEmbeddingProvider は new OpenAI({ apiKey }) が作る SDK 既定のクライアントを使う——このクライアント自身が 429・5xx 等を内部で 再試行する(実測: [email protected] は既定 maxRetries: 2=最大3回・timeout: 600000ms)。 LLMProvider/EmbeddingProvider の「自体はリトライを内蔵しない」は、mnemora の provider コードが再試行を書いていない、という意味であり、SDK が裏で再試行しないという意味ではない。 この数値は SDK の既定値であり mnemora の契約ではないので、SDK の版が上がれば変わりうる。 再試行の回数・timeout を変えたい場合は、自分で作った OpenAI インスタンスを client に 渡す:

import OpenAI from "openai";
import { OpenAILLMProvider } from "@mnemora/openai";

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, maxRetries: 0, timeout: 60_000 });
const llmProvider = new OpenAILLMProvider({ model: "gpt-4o-mini", client });

⚠ 2026-10-01 追記(ADR 0445): timeout は試行ごとに効く。 応答しないサーバーへ timeout: 300, maxRetries: 2 で当てると、合計で約2.2秒かかった(3回試行・間の待ちを含む。擬似サーバーと実物の SDK、[email protected])。⟹ 最悪の合計時間は timeout × (maxRetries + 1) に、再送の待ち(指数バックオフ・retry-after)を足したものであり、既定(timeout: 600000・maxRetries: 2)なら 30 分を超えうる。timeout: 60_000 だけを設定して maxRetries を既定のままにすると、最悪で約3分待つ。呼び出し全体の上限が欲しいなら、signal(AbortSignal.timeout(ms))を opts に渡す(下の「signal(abort)を直に渡したときの振る舞い」)。

⚠ 同じく 2026-10-01 追記(ADR 0445): 再送で治る失敗と治らない失敗がある。 SDK が再送するのは 429・5xx と、応答を受け取る前の接続の失敗である。200 のヘッダを受け取った後に本文が途中で切れた場合は再送されず、素の TypeError(terminated)になる——embed のジョブなら failed で終わり、reembed で回復する。mnemora の provider の側で再送する形にはしていない(Phase 1 に自動リトライは無い。ADR 0032・ADR 0157)。また SDK の再送には冪等キーが付かない(x-stainless-retry-count だけ)ので、プロバイダ側では呼び出しが2回に数えられうる(mnemora が書くのは1回だけ)。

⚠ 2026-09-27 追記: この例は openai を自分の依存として入れないと動かない(pnpm では Cannot find package 'openai')。

🔴 2026-09-29 訂正(Issue #1221、ADR 0350): 上の「同じ版を入れること」はもう要らない。 client の型は Pick<OpenAI, "chat">/Pick<OpenAI, "embeddings">(openai パッケージのクラスをそのまま切り出した型)から、@mnemora/openai 自前の構造型(OpenAIChatClient/OpenAIEmbeddingsClient、openai パッケージの型を一切参照しない)へ変わった。openai を自分の依存として入れる版は、@mnemora/openai が固定している版(7.10.0)と揃える必要が無い——pnpm add openai・npm i openai で最新を入れても、OpenAI インスタンスはそのまま client に渡せる(版ごとの RequestOptions/NullableHeaders の食い違いは、構造型が SDK のクラスを名指ししなくなったことで解消した)。旧型 Pick<OpenAI, "chat">/Pick<OpenAI, "embeddings"> を自分の型注釈にそのまま書いていても、OpenAI/OpenAIChatClient の代入関係は壊れていない——ただし公開の宣言自体を指す型注釈(例: 独自の偽 client の型を OpenAILLMProviderOptions["client"] から typeof で取り出す等)は新しい型名を参照するよう直すこと。移行の詳細は CHANGELOG.md の [1.1.0] 節を見ること。

⚠ 2026-09-27 追記: embed() に渡せる入力の境界(実 API で当てた、今の振る舞い)

OpenAIEmbeddingProvider.embed は、入力を検査せずにそのまま embeddings.create へ1回で渡し(dimensions は常に付く)、 OpenAI のサーバが拒めば、その例外(SDK の BadRequestError、HTTP 400)がそのまま伝わる。どれも mnemora の約束として 決めた値ではなく、OpenAI のサーバの振る舞いである(サーバが変われば変わりうる)。

【実測 2026-09-27、text-embedding-3-small、[email protected]、各1回】

| 入力 | 結果 | | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 空文字 "" | 400「input cannot be an empty string」。1件でも空文字が混ざると、そのバッチ全体が失敗する(["a", ""] も 400) | | 空白だけ(" ") | ベクトルが返る | | 上限(8192 トークン)を超える入力 | 400「maximum input length is 8192 tokens」(dimensions 付きの呼び出しでも同じ、ADR 0305) | | 1回に 2049 件以上 | 400「array length must be 2048 or less」(2048 件は通る)。分割はしない | | dimensions がモデルの上限を超える(text-embedding-3-small に 1537) | 400「Must be less than or equal to 1536」。構築時には分からず、最初の embed() で分かる | | 並び順 | 応答の index は入力の順([0,1,2])で、同じ入力には同じベクトルが返った。embed は index で並べ直して返す |

mnemora の runtime は、recall のクエリを trim して空なら埋め込まず、embed ジョブは1件ずつ渡すので、 空文字の recall と 2049 件以上は runtime からは起きない。ただし Memory の本文(content)や RuntimeDeps.embeddingInput の戻り値が空文字だと、その embed ジョブは 400 で失敗する (@mnemora/local-embedding は空文字にもベクトルを返す——provider で振る舞いが違う)。

⚠ 2026-09-27 追記(Issue #1148): completeStructured に渡せる zod の形(当時の振る舞い。2026-09-29 に変えた——下の追記を見ること)

【実測 2026-09-27、gpt-4o-mini、[email protected]、各形1回】当時は、OpenAILLMProvider.completeStructured が渡された zod スキーマを翻訳してそのまま送っており、送る前に「OpenAI が受け付ける形か」を検査していなかった。受け付けない形は、送った後に OpenAI が拒み、SDK の BadRequestError(HTTP 400、type: invalid_request_error、param: response_format)がそのまま伝わっていた (OpenAILLMProviderError の kind には入らなかった)。

| zod の形 | 結果(2026-09-27 当時) | | ------------------------------------------------------------------------ | ---------------------------------------------------------- | | z.object・z.array・z.enum・optional・nullable(core が使う形) | 通る(core の4つのスキーマは実 API で確かめた、#1164) | | 根が union(判別可能ユニオンなど) | 通る(1つの欄 result を持つ object に包んで送る、#1147) | | z.lazy(再帰) | 通る | | default | 通る | | z.record | 400「'propertyNames' is not permitted」 | | z.tuple | 400「array schema items is not an object」 | | z.date | 400「schema must have a 'type' key」 | | transform | 400「schema must have a 'type' key」 |

⚠ 2026-09-28 追記: 「根が union」と「z.lazy(再帰)」は別々に通ったが、根の union が自分自身を再帰で含む形 (子に根の union を持つ)は、包むときに根を指す参照($ref: "#")を書き換えないので、子の参照が包みの object ({ result: … })を指す——送る形が元のスキーマと変わる(翻訳の結果で確かめた。実 API には当てていない)。 core の4つのスキーマはこの形を使わない。歯は src/__tests__/structured-root-union.test.ts。

拒まれたときの文面はスキーマの位置だけで、プロンプトの本文と API キーは載らなかった(確かめた)。

🔴 2026-09-29 訂正(ADR 0360): 送る前に検査するようになった

上の「送る前には検査しない」はもう成り立たない。completeStructured はいまや、実際に送る JSON Schema を chat.completions.create を呼ぶ前に、openai SDK 自身の strict 変換 toStrictJsonSchema(openai/lib/transform。戻り値は使わず、検査のためだけに呼ぶ ——送るのは今までどおり mnemora 自身の翻訳結果である)に通す。加えて、翻訳そのもの(z.toJSONSchema)も zod の既定(unrepresentable 省略= throw)で行うようになった(以前は unrepresentable: "any" を渡し、z.date()・transform を型の無いスキーマとして黙って 送っていた)。

| zod の形 | いまの結果(2026-09-29 以降) | | --- | --- | | z.object・z.array・z.enum・optional・nullable(core が使う形) | 通る(変わらない) | | 根が union(判別可能ユニオンなど)・z.lazy(再帰)・default | 通る(変わらない) | | z.record | 送る前に OpenAILLMProviderError(kind: "schema_unsupported")——toStrictJsonSchema が must set additionalProperties: false で投げる | | z.tuple | 送る前に OpenAILLMProviderError(kind: "schema_unsupported")——toStrictJsonSchema が unsupported keyword prefixItems で投げる | | z.date | 送る前に OpenAILLMProviderError(kind: "schema_unsupported")——zod 自身が Date cannot be represented in JSON Schema で投げる | | transform | 送る前に OpenAILLMProviderError(kind: "schema_unsupported")——zod 自身が Transforms cannot be represented in JSON Schema で投げる |

どの場合も chat.completions.create は呼ばれず、元の例外は OpenAILLMProviderError.cause(ES2022 の Error.cause)に載る。 上の 2026-09-27 の実測(実 API が 400 で拒む)は、いまは踏まない経路になった——記録として残すが、現物の振る舞いはこの節が正。 確かめていないこと: toStrictJsonSchema が拾わない、実 API だけが拒む形(今回の4形には無かった)が他にあるかは分からない ——この歯は「OpenAI SDK 自身の strict 検査を通るか」までしか保証しない。歯は src/__tests__/structured-output-zod-shapes.test.ts・ src/__tests__/core-schemas-send-shape.test.ts。@mnemora/anthropic も z.record・z.tuple・z.date・transform を同じ形 (kind: "schema_unsupported"、cause 付き)で送る前に落とす(2026-09-30 から。あちらの README)。

戻りの null の扱い(2026-09-28 追記)

strict への翻訳は .optional() の欄を「必須 + null 許容」にして送るので、戻りの null は次のように読む。

| スキーマの位置 | モデルが null を返したとき | | --- | --- | | .optional() の欄 | 省略(キーが無い)として返る | | 必須の .nullable() の欄・.nullable() の配列の要素・根の .nullable() | null のまま返る | | .nullable().optional() の欄 | 省略として返る(null のままにはならない。Issue #1082——翻訳が足した null と区別できないため) |

⚠ 以前は、上の表の2行目の null も消していたので、nullable の欄は ZodError になっていた(上の表の「通る」は送る側の話だった)。 いまは、null を消して検査して落ちたときだけ、スキーマが許す null を残して検査し直す(通る入力の結果は変えず、それでも落ちれば最初の ZodError を投げる)。union の枝ごとに扱いが割れる欄の null は消す側に倒す。@mnemora/anthropic は null をそのまま検査する。 歯は src/__tests__/structured-nullable-roundtrip.test.ts。

上の表に無い形の、今の振る舞い(2026-10-01 追記、ADR 0471)

穴探し39巡目(PR #1576 の ADR の材料1〜5)で、擬似の client に当てて確かめた。 今の振る舞いの記録であって、約束ではない(変えるかどうかはオーナーの判断が要る。どれも、変えると断る入力が増えるか、送る形が変わる)。 射程は送る形と、返った値の検査まで——OpenAI の実 API が、送った JSON Schema を受けるかは確かめていない。core が渡す4つのスキーマは、下のどの形も使っていない。

| zod の形 | 送る JSON Schema | 返った値の扱い | @mnemora/anthropic | | --- | --- | --- | --- | | z.any()・z.unknown() | 型の無い {}(toStrictJsonSchema を通り、送る) | どんな値でも通る | 送る前に schema_unsupported で落ちる(割れる) | | .nullable().optional() | anyOf: [{ type: ["string", "null"] }, { type: "null" }](null が2回入る) | — | type: ["string", "null"](重ならない) | | z.null().optional() | type: ["null", "null"](JSON Schema の type の配列は重複を許さない) | — | type: "null" | | .catchall(T) | additionalProperties: false(T は送らない) | strict を守るサーバからは余分な欄は返らないので、T の値は来ない。来れば T で検査して残る | 同じ | | .nullable().default(v) | type: [..., "null"] と default | null が返ると v になる(null を省略へ戻してから検査するので、default が働く) | null のまま返る(割れる) | | 入力と出力の型が違う pipe(例: z.string().pipe(z.coerce.number())) | 出力側の型(number)で送る | 検査は入力側(string)なので、モデルが送った形どおりに number を返すと ZodError | 同じ |

歯は src/__tests__/structured-output-zod-shapes.test.ts の「上の表に無い形の、今の振る舞い」。

⚠ 2026-09-30 追記(ADR 0428): signal(abort)を直に渡したときの振る舞い

complete / completeStructured / embed の opts.signal を、provider を直に呼んで abort すると、reject する値は signal.reason(reason 無しの abort() なら AbortError の DOMException)である。SDK の APIUserAbortError には ならない。呼ぶ前に abort 済みなら、SDK を呼ばず(リクエストを送らず)に reject する。SDK の再試行待ち (429 の retry-after 等)の最中でも、abort で即座に打ち切られる。signal は SDK にも渡すので、裏のリクエストも切れる。 signal を渡さなければ、今までどおり返るまで待つ。失敗の判定は isOpenAILLMProviderError(kind、無ければ name で見る。kind の値は openai と anthropic で重なるので、name が文字列ならそれが "OpenAILLMProviderError" であることも見る。instanceof を使わない)でもできる。

もっと詳しく

  • docs/architecture.md §3.8・§5.4・§5.5 — LLMProvider / EmbeddingProvider の契約
  • ADR 0019 — 本物の OpenAI を使った計測のコスト
  • ADR 0051 — 本物の provider と記録・擬似 provider の使い分け
  • リポジトリ: https://github.com/takecchi/mnemora