@reinsjs/lowering-fetch

中文文档
0.5.1发布于 2026/11/02MITTypeScriptES Module

reins — zero-dependency lowering layer on fetch: events ⇄ OpenAI Chat Completions / Anthropic Messages / OpenAI Responses, hand-written SSE, no provider SDK

原始使用文档
文档目录8 节
  1. Choosing between @reinsjs/lowering-fetch and @reinsjs/lower…
  2. Runtime
  3. Options
  4. How events land on Chat Completions
  5. How events land on Anthropic Messages
  6. How events land on OpenAI Responses
  7. Errors and retries
  8. Documentation

@reinsjs/lowering-fetch

A zero-dependency lowering layer for reins: timeline events → provider wire protocol → event drafts, using nothing but fetch and a hand-written SSE parser. No provider SDK, no node:*, the same code on Node, Workers, Deno and Bun.

Three wire protocols: OpenAI Chat Completions (the front door of the OpenAI-compatible ecosystem: DeepSeek, Qwen, vLLM, gateways), Anthropic Messages (mid-conversation system placement, signed thinking replay, explicit cache breakpoints) and OpenAI Responses (stateless store: false, encrypted reasoning replay, developer messages anywhere).

pnpm add @reinsjs/lowering-fetch
import { anthropic, anthropicMessages, deepseek, openai, openaiChat, openaiResponses, chatCompletions } from "@reinsjs/lowering-fetch"
import { createAgent } from "@reinsjs/agent"

const opus = anthropic("claude-opus-5", { apiKey: process.env.ANTHROPIC_API_KEY! })
const ds = deepseek("deepseek-flash", { apiKey: process.env.DEEPSEEK_API_KEY! })
const gpt = openai("gpt-5-mini", { apiKey: process.env.OPENAI_API_KEY!, requestOptions: { reasoning: { effort: "low", summary: "auto" } } })
const mini = openaiChat("gpt-4o-mini", { apiKey: process.env.OPENAI_API_KEY! })
// any Anthropic-protocol endpoint (DeepSeek's compatible port, a gateway)
const dsClaude = anthropicMessages("deepseek-v4-flash", {
  provider: "deepseek",
  baseUrl: "https://api.deepseek.com/anthropic",
  apiKey: process.env.DEEPSEEK_API_KEY!,
  reasoning: true,
  midConversationSystem: true,
})
const qwen = chatCompletions("qwen-max", {
  provider: "qwen",
  baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
  apiKey: process.env.DASHSCOPE_API_KEY!,
  contextWindow: 128_000,
})

export const agent = createAgent({ model: ds, store, tools })

Each factory returns a BoundModel = { model, lowering }. apiKey is required and never read from the environment. For several models on one lowering instance, construct FetchLowering yourself and pass models.

Choosing between @reinsjs/lowering-fetch and @reinsjs/lowering-pi

Both implement the same Lowering interface from @reinsjs/core and both declare every landing in a LOSS_MATRIX; createAgent does not care which one you pass. They coexist on purpose — pick per host, not per project.

@reinsjs/lowering-fetch@reinsjs/lowering-pi
ProtocolsOpenAI Chat Completions, Anthropic Messages, OpenAI ResponsesAnthropic Messages, OpenAI Responses
Deferred tools (Anthropic defer_loading + tool_reference, used by lazyTools())Yes — capabilities.deferredTools on official Anthropic modelsNo (request shaping is pi-ai's; references are rendered as text)
Dependencies@reinsjs/core only (≈90 KB of ESM)pi-ai and its ten dependencies (≈65 MB installed)
Request bodypayload.body is what is sentpre-rewrite shape; the real body is produced in pi-ai's onPayload hook
Mid-conversation system on Anthropicplaced by the encoderplaced by rewriting pi-ai's payload
Unsigned thinkingdropped (declared)may fall back to visible text
Model tablea minimal built-in table you override per modelpi-ai's table, maintained upstream
Runtimeverified on Cloudflare workerd at the strictest tier (2023 compatibility date, no nodejs_compat), Bun, Deno (--allow-net only) and Vercel edge-runtime, all three protocols against live providersverified on the same four runtimes, both protocols; Deno needs --allow-sys=osRelease, an Edge runtime needs a process global for the OpenAI path

Start with lowering-fetch for a new host: it covers everything lowering-pi covers plus the OpenAI-compatible ecosystem, ships nothing but this package, and the request you debug is the request that was sent. Stay on lowering-pi when you already run on it (there is nothing to migrate for), or when you want the provider table and new wire-protocol features to arrive through pi-ai rather than through this package. Events written by one layer replay through the other: replay.thinkingSignature uses the same shape on both, so switching does not invalidate a stored timeline.

Runtime

No node:*, no process, no Buffer — the same dist/index.js runs on Node ≥ 22 and on Cloudflare Workers with a 2023 compatibility_date and no compatibility flags (spikes/edge-runtime-check, tier "最严档": module load, a byte-sliced SSE stream against a local fake endpoint, and one live request per protocol). Bun 1.4, Deno 2.9 (only --allow-net needed — the layer reads no system information) and Vercel's edge-runtime (bare vm, no process, no eval) pass the same probes, all three protocols against live providers (spikes/runtime-matrix).

Options

optionmeaning
apiKeyrequired; use auth: "none" when the credential lives in headers (Cloudflare AI Gateway's cf-aig-authorization)
baseUrlprotocol root; /chat/completions (OpenAI: …/v1, DeepSeek: the bare host), /messages (Anthropic: …/v1) or /responses (OpenAI: …/v1) is appended
contextWindow, maxOutputTokens, reasoning, images, cost, midConversationSystemoverride the built-in table; unknown models start from conservative defaults (128k / 16k / no reasoning / no images)
chat.reasoningContentDeepSeek dialect: replay reasoning_content (on by default in deepseek(), see below)
chat.liftToolImageswhen the model takes images, move images in tool results into a user message right after the batch of tool messages (default true); set false for an upstream that rejects that shape and images become a placeholder again
anthropic.betasvalues for the anthropic-beta header; sent only when set (mid-conversation system needs none)
anthropic.cacheBreakpoints, anthropic.cacheTtl, anthropic.midSystemCacheBreakpointexplicit cache breakpoints on Anthropic (default on, 5 min); what to do when a note ends the request — "automatic" (default, top-level cache_control), "previous-user", "drop"
anthropic.deferredToolswhether ToolSpec.deferLoading becomes defer_loading: true and tool_reference content parts become native tool_reference blocks. Default: on for provider: "anthropic", off for third-party Anthropic-protocol endpoints (DeepSeek's compatible endpoint ignores defer_loading, so every tool would be visible). When off, deferred tools are simply not sent and references are rendered as text
responses.systemRolerole for the system prompt and notes on Responses: default developer for reasoning models, system otherwise; pin it for an upstream that rejects one of them
responses.encryptedReasoningdefault on for reasoning models: include: ["reasoning.encrypted_content"] is always sent so reasoning items can be replayed under store: false; off for upstreams that reject include (then thinkingReplay reports false)
requestOptionsspread into the body (max_tokens, temperature, thinking, OpenAI's parallel_tool_calls, Responses' reasoning / max_output_tokens / prompt_cache_key…); messages / input / tools / system / model / stream / store / previous_response_id cannot be overridden. On Anthropic max_tokens defaults to the model's maxOutputTokens and thinking is left to you (Opus 5+ defaults to adaptive server-side; Haiku 4.5 still needs budget_tokens); on Responses reasoning is likewise yours (gpt-5 defaults to medium, gpt-5.1+ to none)
timeoutMswhole-request deadline including the stream, default 600 000; a timeout is a retryable error, a host signal abort is aborted
trustMarkersdefault on: tool output is wrapped in <untrusted source="tool:…"> in the request; the log keeps the original

How events land on Chat Completions

Every landing is declared in LOSS_MATRIX["openai-chat"] and asserted by tests — nothing is dropped silently.

  • system_note → a mid-conversation system message (exact; verified on OpenAI and DeepSeek). Set midConversationSystem: false for an upstream that rejects it and the note goes out as a framed user message (lossy). A note marked directive: true (an instruction the model must act on, such as compact's request) always goes out as the framed user message, on all three protocols.
  • A user message or note that arrives while tool results are still pending is moved after them on the wire (tool messages must directly follow the assistant that called them); the user message is recorded lossy(user), the log order is untouched.
  • Thinking: Chat Completions has no replay slot (no signature, no encrypted item). On the official API thinking events are dropped. With chat.reasoningContent (DeepSeek) they are replayed as reasoning_content — and DeepSeek requires the field on every historical assistant message once tools is present (400 otherwise), so the layer always sends it, empty when there is nothing to replay.
  • One turn with several text blocks is merged into one content string (lossy(merged-text)); tool messages carry text only: when the model takes images, each image leaves a pointer ([image n attached in the next user message]) and the images go into one user message right after the whole batch of tool messages — before any user message that was moved after the batch — with a line per result naming the tool, the call id and the image count and saying it is tool output, not the user; untrusted results keep their <untrusted> marking (lossy(tool+lifted-images); verified on DeepSeek's official API — check other OpenAI-compatible upstreams before relying on it, and turn it off with chat.liftToolImages: false if they reject the shape). Without image support, or with chat.liftToolImages: false, images become a placeholder (lossy(tool-text-only)); isError has no flag on this protocol and is expressed as a [tool error] prefix (lossy).
  • No explicit cache breakpoints exist on this protocol; provider-side automatic prefix caching applies and cached_tokens is reported as cacheRead.
  • Usage follows core semantics: input is the uncached prompt count. costUsd uses the built-in price table (DeepSeek: peak rates, an upper bound).

How events land on Anthropic Messages

Declared in LOSS_MATRIX["anthropic-messages"], cell for cell comparable with @reinsjs/lowering-pi.

  • system_note → a mid-conversation { role: "system" } message on model families that accept it (Fable 5.x / Mythos 5.x / Opus 5 / Opus 4.8, or midConversationSystem: true). The provider requires such a message to follow a user turn and be followed by an assistant turn or end the request, so notes are held and emitted right before the next assistant turn (or at the end); when that slot follows an assistant turn — or the note would be the first message — the note goes out as a framed user text (lossy(user-role)). Other models always get the framed text.

  • Tool results and anything moved after them (a user message that arrived while results were pending) share one user message, so tool_result blocks directly follow the tool_use turn. is_error is carried natively.

  • Thinking is replayed only when the event carries a signature from the same provider, API and model (thinking / redacted_thinking blocks); unsigned thinking (an interrupted stream), thinking from another provider, or thinking from another model of the same provider is dropped and declared — it is not turned into visible text (a signature is only valid for the model that produced it: replaying one across models is a 400). Empty-text thinking blocks with a signature (display: "omitted") are kept and replayed.

  • Cache breakpoints: one on the last system block, one on the last tool, one on the last block of the last user message. When a note is the last message, the conversation breakpoint becomes a top-level cache_control (measured on par with no injection). Never more than four in total.

  • Usage: input_tokens is already the uncached count; cache_read_input_tokens / cache_creation_input_tokens map to cacheRead / cacheWrite. stop_reason: "refusal" becomes a non-retryable error carrying stop_details.

  • Deferred tools (ToolSpec.deferLoading, set by lazyTools() when capabilities.deferredTools is true): defer_loading: true on the tool, the cache breakpoint moves to the last non-deferred tool (a deferred tool cannot carry cache_control), and if every tool would be deferred none is (the API rejects that). A tool_result whose content is only tool_reference parts, comes from a system-trusted result, and names tools present in this request's tools is sent as tool_reference blocks the API expands in place — the tool list never changes, so the cache prefix survives (exact / tool-reference); text parts in the same result are moved after the batch of tool_result blocks (lossy / tool-reference, they cannot be mixed). References to tools not in this request, or inside untrusted results, are rendered as text instead — the API validates every reference in the whole history and answers 400 for an unknown name.

How events land on OpenAI Responses

Declared in LOSS_MATRIX["openai-responses"], cell for cell comparable with @reinsjs/lowering-pi.

  • Stateless by construction. Every request carries the whole history in input; store: false is forced and a previous_response_id in requestOptions is dropped — the timeline is the only source of truth, OpenAI's server-side conversation state is never relied on.
  • The system prompt and system_note are developer messages (reasoning models) or system messages (others) at any position in input — no placement rules, always exact. compaction is a framed user text (lossy).
  • Reasoning: reasoning models always request include: ["reasoning.encrypted_content"]; each reasoning item is stored whole in the event's replay.thinkingSignature and replayed verbatim (the provider verifies the encrypted payload — a forged one is a 400). Items without encrypted_content (an interrupted stream, include refused), from another provider, or from another model are dropped and declared, never turned into text. Reasoning items with an encrypted payload but no summary are kept as empty-text thinking events so the next turn can replay them.
  • Assistant text becomes a type: "message" item per text block, replaying the provider's msg_ id (replay.textSignature, lowering-pi's JSON form is accepted too) or a generated one. Tool calls are function_call items: call_id is the event's toolCallId, the fc_ item id lives in replay.itemId and is replayed only for the same model (OpenAI validates the pairing of fc_ and rs_ items); arguments is a JSON string, so non-object arguments need no wrapping. Tools are sent with strict: false.
  • Tool results are function_call_output items matched by call_id: a string when text-only, an array of input_text / input_image blocks when the result has images and the model accepts them; isError has no flag on this protocol and is expressed as a [tool error] prefix (lossy).
  • Usage: input_tokens includes cached and cache-write tokens, so input is input_tokens − cached_tokens − cache_write_tokens, with the two mapped to cacheRead / cacheWrite. status: "incomplete" with max_output_tokens is length; with content_filter a non-retryable error; response.failed and error frames are errors carrying the provider's code and message. Automatic prefix caching is best-effort on OpenAI's side; pass prompt_cache_key in requestOptions to improve hit rates.
  • Built-in table: the gpt-5 family (5.5 / 5.4 / 5.4-mini / 5.2 / 5.1 / 5 / 5-mini / 5-nano), o3 / o4-mini and gpt-4.1 / 4.1-mini / 4o-mini on Responses; the same OpenAI ids also exist as Chat Completions entries, which openaiChat() picks by protocol. A bare { provider: "openai", id } on a FetchLowering you construct yourself resolves to Responses.

Errors and retries

Non-2xx responses throw HttpError with status, headers and the raw body ("<status> <body>", the same shape the official SDKs use), so runLoop's transient-failure rule applies unchanged: 408 / 409 / 429 / 5xx retry, everything else does not. Network failures propagate from fetch untouched.

Documentation

docs/archive/implementation-plans/2026-09-08-技术方案v1.0.md §11, docs/系统全景图_模块盘点/lowering-fetch.md, spikes/f1-chat-live/, spikes/f2-anthropic-live/, spikes/f3-responses-live/ (live verification against DeepSeek and, through the Cloudflare AI Gateway, OpenAI and Anthropic) and spikes/edge-runtime-check/ (Cloudflare workerd) — in Chinese, at the repository root.

MIT © 2026 NewRate Limited.

版本与发布记录

@reinsjs/lowering-fetch 共发布 7 个版本,首次发布于 2026/09/15;过去 12 个月发布了 7 个版本。

版本发布日期距今
0.5.1latest2026/11/021 天前
0.5.02026/10/313 天前
0.4.12026/10/303 天前
0.4.02026/10/277 天前
0.3.02026/09/221 个月前
0.2.12026/09/151 个月前

关于 @reinsjs/lowering-fetch 的常见问题

@reinsjs/lowering-fetch 是什么?

缰—获取上的零依赖降低层:事件当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种当下一种的当下一种

如何安装 @reinsjs/lowering-fetch?

在项目目录运行 npm install @reinsjs/lowering-fetch,或使用 pnpm add / yarn add / bun add。

@reinsjs/lowering-fetch 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@reinsjs/lowering-fetch 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@reinsjs/lowering-fetch 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@reinsjs/lowering-fetch 可以商用吗?

@reinsjs/lowering-fetch 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@reinsjs/lowering-fetch 还在维护吗?

最新版本 0.5.1 发布于 2026/11/02(1 天前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/09/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。