@reinsjs / lowering-fetch
的零依赖降低层 缰绳:时间线事件→提供程序连接协议→事件草稿,只使用 fetch 以及一个手写的SSE解析器。没有供应商SDK,没有 node:*在Node、Workers、Deno和Bun上使用相同的代码。
三种有线协议: OpenAI聊天完成 (兼容openai生态系统的前门:DeepSeek、Qwen、vLLM、网关) 人为的消息 (会话中系统放置,签名思考重播,显式缓存断点)和 OpenAI反应 (无状态 store: false、加密推理回放、开发者消息等)。
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 })
每个工厂返回一个 BoundModel = { model, lowering }. apiKey 是必需的,并且从不从环境中读取。对于一个降低实例上的多个模型,构造 FetchLowering 你自己和传球 models.
选择 @reinsjs/lowering-fetch 和 @reinsjs/lowering-pi
两者实现相同 Lowering 界面与 @reinsjs/core 每一次降落都是a LOSS_MATRIX; createAgent 不在乎你经过哪一个。它们是有意共存的——选择每个主机,而不是每个项目。
@reinsjs/lowering-fetch | @reinsjs/lowering-pi | |
|---|---|---|
| 协议 | OpenAI聊天完成,Anthropic消息,OpenAI响应 | Anthropic Messages, OpenAI Responses |
延期工具(人为的) defer_loading + tool_reference,用于 lazyTools()) | 是的, capabilities.deferredTools 官方Anthropic模型 | 没有(请求整形是pi-ai的;引用以文本形式呈现) |
| 依赖关系 | @reinsjs/core 仅(≈90 KB的ESM) | pi-ai 及其10个依赖项(安装约65 MB) |
| 请求体 | payload.body 是发送的内容 | pre-rewrite形状;真正的身体是在pi-ai中产生的 onPayload 钩 |
在交谈 system 在人为 | 由编码器放置 | 通过重写pi-ai的有效载荷来放置 |
| 无符号思维 | dropped (声明) | 可以退回到可见文本 |
| 模型表 | 每个模型覆盖的最小内置表 | Pi-ai的桌子,维护在上游 |
| 运行时 | 在最严格的层(2023兼容日期,no . 5)的Cloudflare workerd上验证 nodejs_compat)、Bun、Deno (--allow-net 只有)和Vercel edge-runtime,针对实时提供商的所有三个协议 | 在相同的四个运行时上验证,两个协议;Deno需求 --allow-sys=osRelease, Edge运行时需要 process OpenAI路径的全局 |
从……开始 lowering-fetch 对于一个新主持人来说,它涵盖了一切 lowering-pi 覆盖和openai兼容的生态系统,除了这个包什么都没有,你调试的请求就是发送的请求。呆在 lowering-pi 当您已经在它上运行时(没有什么需要迁移的),或者当您希望提供程序表和新的有线协议特性通过pi-ai而不是通过这个包到达时。由一层编写的事件通过另一层重放: replay.thinkingSignature 在两者上使用相同的形状,因此切换不会使存储的时间轴失效。
运行时
否 node:*,没有 process,没有 Buffer -一样 dist/index.js 运行在节点≥22和Cloudflare worker上的2023 compatibility_date 没有兼容性标志(spikes/edge-runtime-check层:模块负载,针对本地假端点的字节切片SSE流,以及每个协议一个实时请求)。Bun 1.4, Deno 2.9(仅限) --allow-net 需要-层不读取系统信息)和Vercel的 edge-runtime (裸vm,不 process,没有 eval)通过相同的探测,所有三个协议都针对活动提供商(spikes/runtime-matrix)。
选项
| 选项 | 意义 |
|---|---|
apiKey | 要求;使用 auth: "none" 当凭证存在时 headers (Cloudflare AI Gateway的 cf-aig-authorization) |
baseUrl | 协议根; /chat/completions (OpenAI: …/v1(DeepSeek:裸主机); /messages (人为: …/v1)或 /responses (OpenAI: …/v1)是附加的 |
contextWindow, maxOutputTokens, reasoning, images, cost, midConversationSystem | 覆盖内置表;未知模型从保守默认值开始(128k / 16k /无推理/无图像) |
chat.reasoningContent | DeepSeek方言:重放 reasoning_content (默认在 deepseek(),见下文) |
chat.liftToolImages | 当模型获取图像时,将工具结果中的图像移动到a中 user 消息后的批次 tool 消息(默认 true); 集 false 对于上游,拒绝形状和图像再次成为占位符 |
anthropic.betas | 的值 anthropic-beta 头;只在设置时发送(会话中系统不需要) |
anthropic.cacheBreakpoints, anthropic.cacheTtl, anthropic.midSystemCacheBreakpoint | 显式缓存断点在Anthropic(默认开启,5分钟);当请求结束时该怎么办 "automatic" (默认,顶级 cache_control), "previous-user", "drop" |
anthropic.deferredTools | 是否 ToolSpec.deferLoading 变成了 defer_loading: true 和 tool_reference 内容部分变成原生的 tool_reference 块。默认值:开启 provider: "anthropic",对于第三方Anthropic-protocol端点(DeepSeek的兼容端点忽略)关闭 defer_loading,所以每个工具都是可见的)。当关闭时,延迟的工具将不会被发送,并且引用将被呈现为文本 |
responses.systemRole | 应答中的系统提示和注释的角色:default developer 对于推理模型, system 否则;把它钉在上游,拒绝其中一个 |
responses.encryptedReasoning | 推理模型默认为: include: ["reasoning.encrypted_content"] 是不是总是发这样的推理题下可以重播 store: false;关闭拒绝的上游 include (当时 thinkingReplay 报告 false) |
requestOptions | 扩散到体内(max_tokens, temperature, thinking, OpenAI parallel_tool_calls,反应 reasoning / max_output_tokens / prompt_cache_key…); messages / input / tools / system / model / stream / store / previous_response_id 不能被覆盖。在人为 max_tokens 默认为模型的 maxOutputTokens 和 thinking 留给你(Opus 5+默认为自适应服务器端;Haiku 4.5仍然需要 budget_tokens); 在反应 reasoning 你的(gpt-5默认值是? medium, gpt-5.1+ to none) |
timeoutMs | 整个请求截止日期包括流,默认为600000;超时是一个可重试的错误,一个主机 signal 流产是 aborted |
trustMarkers | 默认开启:工具输出被封装在 <untrusted source="tool:…"> 在请求中;日志保存原件 |
事件如何在聊天完成时发生
每次降落都要申报 LOSS_MATRIX["openai-chat"] 并由测试断言——没有什么是无声地掉落的。
system_note→谈话中途system消息(exact;在OpenAI和DeepSeek上验证)。集midConversationSystem: false如果上游拒绝了它,音符就会以框架的形式发出user消息(lossy). 一张写着directive: true(模型必须执行的指令,例如compact(他的请求)总是被框出去user消息,在所有三个协议上。- 当工具结果仍然挂起时到达的用户消息或注释在连线上移动到它们之后(
tool消息必须直接跟随assistant这叫他们);记录用户留言lossy(user),日志顺序不变。 - 思考聊天完成没有重放槽(没有签名,没有加密项目)。关于官方API的思考事件是
dropped. 与chat.reasoningContent(DeepSeek)它们被重播为reasoning_content——和DeepSeek 需要 每条历史助理信息上的字段tools是存在的(否则是400),所以层总是发送它,当没有什么可重放时为空。 - 一个回合与几个文本块合并为一个回合
content字符串(lossy(merged-text));tool消息只携带文本:当模型拍摄图像时,每个图像都会留下一个指针([image n attached in the next user message]),这些图像合二为一user整批后的消息tool消息-在批处理后移动的任何用户消息之前-每个结果用一行命名工具,调用id和图像计数,并说这是工具输出,而不是用户;不可信的结果保持不变<untrusted>标记(lossy(tool+lifted-images);在DeepSeek的官方API上验证-在依赖它之前检查其他openai兼容的上游,并将其关闭chat.liftToolImages: false如果他们拒绝这个形状)。没有图像支持,或有chat.liftToolImages: false,图像成为占位符(lossy(tool-text-only));isError在此协议上没有标志,表示为[tool error]前缀(lossy)。 - 此协议上不存在显式缓存断点;提供程序端自动前缀缓存应用和
cached_tokens据报道为cacheRead. - 用法遵循核心语义:
input未缓存的提示计数。costUsd使用内置价格表(DeepSeek:峰值费率,上限)。
事件是如何登陆人类信息的
中声明 LOSS_MATRIX["anthropic-messages"],细胞对细胞比较 @reinsjs/lowering-pi.
-
system_note→谈话中途{ role: "system" }关于接受它的模范家庭的信息(表5)。x /神话;x / Opus 5 / Opus 4.8或midConversationSystem: true). 提供者需要这样的消息来跟随user转过身来,后面跟着一个assistant转或结束请求,因此音符在下一个辅助转之前(或在结束时)保持并发出;当这个槽跟随助手的转-或者这张纸条将是第一条信息-这张纸条将作为一个框发出user文本(lossy(user-role)). 其他模型总是得到框架文本。 -
工具结果和在它们之后移动的任何东西(在结果挂起时到达的用户消息)共享一个
user消息,所以tool_result街区直接沿着tool_use转弯。is_error是本地携带的。 -
思考 只有当事件带有
signature来自同一个提供者,API 和模型 (thinking/redacted_thinking块);未签名的思维(中断的流),来自另一个提供者的思维,或者来自同一提供者的另一个模型的思维dropped声明——它不会变成可见的文本(签名只对产生它的模型有效:跨模型重播一个签名是400)。带有签名的空文本思维块(display: "omitted")被保存和重播。 -
缓存断点一个在最后一个系统模块上,一个在最后一个工具上,一个在最后一个模块上
user消息。当注释是最后一条消息时,对话断点将成为顶级断点cache_control(与未注射时相同)。总共不超过四个。 -
用法:
input_tokens已经是未缓存计数;cache_read_input_tokens/cache_creation_input_tokens映射到cacheRead/cacheWrite.stop_reason: "refusal"变成不可重试的error携带stop_details. -
延期工具(
ToolSpec.deferLoading,由lazyTools()当capabilities.deferredTools是真实的):defer_loading: true在该工具上,缓存断点移动到最后一个非延迟工具(延迟工具不能携带)cache_control),如果每个工具都要延迟,那么没有一个工具会延迟(API会拒绝)。一个tool_result它的内容只有tool_reference部件,来自系统信任的结果,并命名此请求中出现的工具tools发送为tool_reference阻止了API的扩展——工具列表永远不会改变,所以缓存前缀会保留(exact/tool-reference); 在同一结果中的文本部分被移动到批后tool_result块(lossy/tool-reference,它们不能混合)。对不在此请求中的工具的引用,或者在不受信任的结果中对工具的引用,将被呈现为文本——API将验证整个历史记录中的每个引用,并对未知名称回答400。
事件如何登陆OpenAI响应
中声明 LOSS_MATRIX["openai-responses"],细胞对细胞比较 @reinsjs/lowering-pi.
- 无国籍建筑。 每个请求都包含整个历史
input;store: false是被迫的previous_response_id在requestOptions时间轴是唯一的真实来源,OpenAI的服务器端会话状态从不依赖。 - 系统提示和
system_note是developer消息(推理模型)或system消息(其他)在任何位置input-永远没有放置规则exact.compaction是一个框架吗?user文本(lossy)。 - 推理:推理模型总是要求
include: ["reasoning.encrypted_content"];每个推理项都完整地存储在事件中replay.thinkingSignature并逐字重播(提供者验证加密的有效载荷—伪造的有效载荷是400)。项目没有encrypted_content(一条中断的小溪,include拒绝),来自另一个提供者,或来自另一个模型dropped而宣言,从未化作文字。具有加密有效负载但没有摘要的推理项目被保存为空文本思考事件,以便下一轮可以重播它们。 - 辅助文本变成了
type: "message"项,重播提供者的msg_id (replay.textSignature(也接受lower -pi的JSON格式)或生成的格式。工具调用是function_call项目:call_id是事件的toolCallId,fc_项目id驻留在replay.itemId并且只对相同的模型重播(OpenAI验证配对fc_和rs_项目);arguments是一个JSON字符串,所以非对象参数不需要包装。工具随附strict: false. - 工具结果如下
function_call_output匹配的项目call_id:纯文本时为字符串,为的数组input_text/input_image当结果包含图像并且模型接受它们时,将阻塞;isError在此协议上没有标志,表示为[tool error]前缀(lossy)。 - 用法:
input_tokens包括缓存和缓存写入令牌,所以input是input_tokens − cached_tokens − cache_write_tokens,两者映射到cacheRead/cacheWrite.status: "incomplete"与max_output_tokens是length;与content_filter一个non-retryableerror;response.failed和error帧是携带提供者代码和消息的错误。自动前缀缓存是OpenAI方面的最大努力;通过prompt_cache_key在requestOptions提高命中率。 - 内置表:gpt-5系列(5.5 / 5.4 / 5.4-mini / 5.2 / 5.1 / 5 / 5-mini / 5-nano), o3 / o4-mini和gpt-4.1 / 4.1-mini / 40 -mini的响应;相同的OpenAI id也存在于聊天完成项中,其中
openaiChat()按协议选择。一个光秃秃的{ provider: "openai", id }在一个FetchLowering你构建自己的决心来回应。
错误和重试
非2xx响应抛出 HttpError 与 status, headers 生的身体("<status> <body>"(与官方sdk使用的形状相同),所以 runLoop408 / 409 / 429 / 5xx可以重试,其他的都不能。网络故障从 fetch 都没动。
文档
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/ (对DeepSeek进行实时验证,并通过Cloudflare AI网关、OpenAI和Anthropic进行验证)和 spikes/edge-runtime-check/ (Cloudflare workerd) -中文,在存储库根目录。
MIT©2026 newate Limited