复杂度从 N×M 降到 N+M —— 协议驱动 + 零依赖的 LLM Provider 基座
From N×M to N+M — Protocol-driven, zero-dependency LLM provider shim
English | 中文
LLM API 只有 3 种协议,但走同一种协议的服务商可以有无数个。把 协议(怎么发请求)和 Provider(连到哪)拆开——复杂度从 N×M 子类爆炸变成 N 行数据 + M 个协议对象。
启发自 unblind 的 Provider 层设计,经过 7 个 Provider、3 个协议族的生产验证。
协议(纯函数,写一次) ← 3 个对象,各自独立演化
Provider(纯数据,一行一个) ← 厂商 + 协议 = 一条注册表条目
模型(字段值,不占条目) ← 环境变量切换,不碰注册表
换模型不改代码。加厂商不加协议。加协议不改注册表。 三个维度独立。
npm install zeshimimport { GenericProvider, PROTOCOLS } from "zeshim";
const provider = new GenericProvider({
name: "openai",
protocol: PROTOCOLS["openai-chat-completions"],
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.OPENAI_API_KEY!,
model: "gpt-4o",
});
const result = await provider.execute({
inputs: [
{ type: "image", data: "data:image/png;base64,...", mimeType: "image/png" }
],
prompt: "这张图里有什么?",
});
console.log(result.content); // → "一只猫坐在窗台上..."
console.log(result.processingTimeMs); // → 1234子类方案:每个 Provider 一个类,每家厂商 × 每个协议 = 一个子类。7 个 Provider 三个协议族 = 7 个子类 + build 函数。
协议方案:协议是纯函数对象(M 个),Provider 是注册表数据(N 行)。同一厂商双协议接入?加一行,不写代码。
| 子类方案 | 协议方案 | |
|---|---|---|
| 同协议加厂商 | 写 build 函数 | 加一行数据 |
| 同厂商加协议 | 写新子类 | 加一行数据 |
| 换模型 | 改字段 ✅ | 改字段 ✅ |
| 协议逻辑单测 | ❌ 需 Key | ✅ 纯函数 |
import { loadProviders } from "zeshim";
const chain = loadProviders("mimo,openai,groq", {
model: "gpt-4o",
timeoutMs: 15_000,
});
for (const { provider } of chain) {
try {
return await provider.execute({ inputs, prompt });
} catch (err) {
if (err.category === "auth") throw err; // 不重试
continue; // 尝试下一个 Provider
}
}同一协议族内不同 Provider 的微小差异通过 overrides 声明,不污染协议定义。允许覆盖全部 5 个 Protocol 函数:auth, buildContent, buildBody, extractContent, parseError。
不管调用哪个 API,错误统一为四类:auth → rate_limit → server → client
GenericProvider — 唯一类,零子类。execute() 一次性调用,sendStream() 流式调用(自动回退)。
loadProviders(order, opts?) — 从 REGISTRY 读取已配置的 Provider,按 order 顺序返回。
sendStream({ inputs, prompt, options?, signal? }) — 返回 AsyncIterable<StreamChunk>。如果 Protocol 未实现原生流式,自动回退到 execute() 后一次性 emit。
for await (const chunk of provider.sendStream({ inputs, prompt })) {
if (chunk.type === "text") process.stdout.write(chunk.content);
if (chunk.type === "done") console.log("\\n--- done ---");
}- 零依赖:纯 TypeScript,Node.js >= 18 内置模块
- 480 LOC:5 个文件,每个职责单一
- 协议纯函数:
buildContent、extractContent等零副作用,可直接单测 - 生产验证:在 unblind 中跑过 7 个 Provider、171 个测试
📖 复杂度从 N×M 降到 N+M(掘金) · From N×M to N+M (dev.to)
zeshim separates protocol (how to call an API family) from provider (which endpoint + key). Complexity drops from N×M to N+M — 3 protocol objects + N registry rows = all providers. Switch models by changing a field, add providers by adding a row, add protocols by adding an object. All three dimensions independent.
Inspired by the provider layer design of unblind, battle-tested across 7 providers and 3 protocol families.
npm install zeshimimport { GenericProvider, PROTOCOLS } from "zeshim";
const provider = new GenericProvider({
name: "openai",
protocol: PROTOCOLS["openai-chat-completions"],
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.OPENAI_API_KEY!,
model: "gpt-4o",
});
const result = await provider.execute({
inputs: [{ type: "image", data: "data:image/png;base64,...", mimeType: "image/png" }],
prompt: "What's in this image?",
});Key Concepts: N+M architecture · 3 built-in protocols · GenericProvider (single class, zero subclasses) · Overrides for per-provider quirks · Error normalization (auth|rate_limit|server|client) · Zero dependencies, 480 LOC.
欢迎提 Issue 和 PR。
git clone https://github.com/Santazuki/zeshim.git
npm install # 仅 TypeScript 编译器
npm run build # 编译到 dist/npm testMIT