06-LLM 多协议抽象的 Protocol 管道

06-LLM 多协议抽象的 Protocol 管道

对应问题 Q6:Protocol<Body, Frame, Event, State> 四类型参数管道、Route.make() 四轴正交组合、compile() 编译边界、cache-policy.ts inline hint 注入、executor.ts 类型化错误映射与预输出重试。 勘察依据:packages/llm/src/route/protocol.tsroute/client.tsroute/executor.tscache-policy.tsroute/endpoint.tsroute/auth.tsroute/framing.tsroute/transport/http.tsprotocols/openai-chat.tsprotocols/anthropic-messages.tsprotocols/openai-compatible-chat.tsproviders/openai-compatible.tsschema/errors.tsschema/events.tsprovider-error.ts

一、Protocol:一个模型服务族的语义 API 契约

@opencode-ai/llm 包的核心设计目标是:将"这个 API 长什么样"(语义契约)与"请求发到哪里、用什么认证、怎么传输"(部署契约)正交分离。Protocol 是前者的载体------它只 owns 三件事:公共 LLMRequest 如何变成 provider 原生请求体、请求体必须满足什么 schema、流式响应如何解码回公共 LLMEvent

1.1 四类型参数的定义

ts 复制代码
// 36:43:opencode/packages/llm/src/route/protocol.ts
export interface Protocol<Body, Frame, Event, State> {
  /** Stable id for the wire protocol implementation. */
  readonly id: ProtocolID
  /** Request side: schema for the provider-native body and how to build it. */
  readonly body: ProtocolBody<Body>
  /** Response side: streaming state machine. */
  readonly stream: ProtocolStream<Frame, Event, State>
}

四个类型参数精确对应管道的四个阶段:

类型参数 管道阶段 语义 生命周期
Body 请求体 provider 原生请求体候选。Route.makebody.schema 校验后 JSON 编码发出 每次 compile 构建
Frame 传输帧 流式响应的一个分帧单元。SSE 是一段 JSON data: 字符串;AWS event stream 是一个解析后的二进制帧 每个分帧周期
Event 原生事件 从一个 Frame 经 stream.event schema 解码后的 provider 原生事件 每帧一个
State 解析状态 贯穿 stream.step 的累加器,将事件序列翻译为 LLMEvent 序列 每次响应一个

1.2 请求侧:ProtocolBody

ts 复制代码
// 45:50:opencode/packages/llm/src/route/protocol.ts
export interface ProtocolBody<Body> {
  /** Schema for the validated provider-native body sent as the JSON request. */
  readonly schema: Schema.Codec<Body, unknown>
  /** Build the provider-native body from a common `LLMRequest`. */
  readonly from: (request: LLMRequest) => Effect.Effect<Body, LLMError>
}

ProtocolBody 有两个字段:

  • schemaBody 类型的 JSON Codec------它既是类型来源 (TypeScript 通过 schema 推断 Body),也是运行时校验器compile 用它 decodeUnknown 确保构建的 body 满足契约)
  • from:把公共 LLMRequest 降级(lowering)为 provider 原生 body 的 Effect 函数。这是唯一 知道公共消息如何映射到 provider 线格式的地方------provider 的 quirks 被锁在这里,不泄漏到 LLMRequest

1.3 响应侧:ProtocolStream

ts 复制代码
// 52:63:opencode/packages/llm/src/route/protocol.ts
export interface ProtocolStream<Frame, Event, State> {
  /** Schema for one decoded streaming event, decoded from a transport frame. */
  readonly event: Schema.Codec<Event, Frame>
  /** Initial parser state. Called once per response with the resolved request. */
  readonly initial: (request: LLMRequest) => State
  /** Translate one event into emitted `LLMEvent`s plus the next state. */
  readonly step: (state: State, event: Event) => Effect.Effect<readonly [State, ReadonlyArray<LLMEvent>], LLMError>
  /** Optional request-completion signal for transports that do not end naturally. */
  readonly terminal?: (event: Event) => boolean
  /** Optional flush emitted when the framed stream ends. */
  readonly onHalt?: (state: State) => ReadonlyArray<LLMEvent>
}

ProtocolStream 是一个流式状态机,五个字段对应五个阶段:

  • eventSchema.Codec<Event, Frame>------从一个传输帧解码出原生事件的 schema。类型上 Frame → Event,是 ProtocolBody.schemaBody ↔ unknown)在响应侧的镜像
  • initial:每个响应调用一次,传入 resolved request,返回初始 State。这通常是 ToolStream.empty() + Lifecycle.initial() 的组合
  • step:核心翻译器------输入 (当前 State, 一个 Event),输出 [下一个 State, 要发射的 LLMEvent 数组]。这是把 provider 的流式语义(OpenAI 的 delta、Anthropic 的 content_block_delta)翻译为公共事件(text-delta、tool-input-delta)的唯一位置
  • terminal:可选------对于不自然结束的传输(如 WebSocket),标记哪个事件表示完成
  • onHalt:可选------当分帧流结束时(如 OpenAI 发完所有 chunk),用最终 State 冲刷出剩余事件(如 Anthropic 没有 onHalt,而 OpenAI Chat 用它发射 finish 事件)

1.4 完整流水线:request → body → frames → events → LLMEvent

将四个类型参数和两块(Body / Stream)组合,完整的请求-响应流水线如下:

bash 复制代码
        ┌─────────────────────────────────────────────────────────────┐
        │                    compile() 编译阶段                         │
        │                                                            │
  LLMRequest ──► applyCachePolicy() ──► resolveRequestOptions()       │
        │                                    │                        │
        │                           route.body.from(resolved)          │
        │                                    │                        │
        │                              Body (候选)                      │
        │                                    │                        │
        │                     Schema.decodeUnknown(body.schema)        │
        │                                    │                        │
        │                              Body (已校验)                    │
        │                                    │                        │
        │                        route.prepareTransport(body)          │
        │                                    │                        │
        │                              Prepared (HTTP 预备)             │
        └────────────────────────────────────┼────────────────────────┘
                                             │
        ┌────────────────────────────────────▼────────────────────────┐
        │              streamPrepared() 流式执行阶段                    │
        │                                                            │
     Prepared ──► transport.frames(prepared, runtime)                 │
                        │                                            │
                   Stream<Frame>  ◄── Framing.sse / bedrock-event     │
                        │                                            │
                Stream.mapAccumEffect                                │
                   ┌────┴────────────────────────┐                   │
                   │  decodeEvent(frame)          │  ← stream.event   │
                   │  → Event                     │     schema 解码     │
                   └────┬────────────────────────┘                   │
                        │                                            │
                   stream.step(state, event)                         │
                        │                                            │
                   [nextState, LLMEvent[]]                           │
                        │                                            │
                   Stream<LLMEvent>  ──► 公共事件流                    │
        └─────────────────────────────────────────────────────────────┘

类型流向不变量LLMRequest → Body(经 body.from),Body → unknown(经 body.schema 编码),HTTP 传输后 Uint8Array → Frame(经 Framing.frame),Frame → Event(经 stream.event 解码),Event + State → LLMEvent[](经 stream.step)。每一步的类型转换都有对应的 Schema.Codec 站岗------schema 既是编译期的类型推断来源,也是运行时的校验闸门。

二、Route.make():四轴正交组合

Route.make 是协议层的核心构造器。它把四个正交的部署维度 组合成一个可执行的 Route:一个 Protocol + 一个 Endpoint + 一个 Auth + 一个 Framing(+ 可选 headers/defaults)。这个分离让 DeepSeek、TogetherAI 等只需复用 OpenAIChat.protocol,零代码即可接入。

2.1 Route 的完整结构

ts 复制代码
// 36:53:opencode/packages/llm/src/route/client.ts
export interface Route<Body, Prepared = unknown> {
  readonly id: string
  readonly provider?: ProviderID
  readonly protocol: ProtocolID
  readonly endpoint: Endpoint<Body>
  readonly auth: AuthDef
  readonly transport: Transport<Body, Prepared, unknown>
  readonly defaults: RouteDefaults
  readonly body: RouteBody<Body>
  readonly with: (patch: RoutePatch<Body, Prepared>) => Route<Body, Prepared>
  readonly model: (input: RouteMappedModelInput) => Model
  readonly prepareTransport: (body: Body, request: LLMRequest) => Effect.Effect<Prepared, LLMError>
  readonly streamPrepared: (
    prepared: Prepared,
    request: LLMRequest,
    runtime: TransportRuntime,
  ) => Stream.Stream<LLMEvent, LLMError>
}

Route 持有四轴的绑定结果(protocol/endpoint/auth/transport),加上两个运行时方法(prepareTransport + streamPrepared)和一个可变性方法(with 用于 patch)。

2.2 四轴正交模型

bash 复制代码
   ┌───────────────────────────────────────────────────────┐
   │                Route.make(input)                      │
   │                                                      │
   │   ┌─────────────┐  ┌──────────────┐                  │
   │   │  Protocol   │  │   Endpoint   │                  │
   │   │ (语义契约)  │  │ (URL 在哪)   │                  │
   │   │ Body/Frame/ │  │ baseURL+path │                  │
   │   │ Event/State │  │ +query       │                  │
   │   └──────┬──────┘  └──────┬───────┘                  │
   │          │                │                          │
   │   ┌──────▼──────┐  ┌──────▼───────┐                  │
   │   │    Auth     │  │   Framing    │                  │
   │   │ (怎么认证)  │  │ (怎么分帧)   │                  │
   │   │ bearer/none │  │ sse/binary   │                  │
   │   │ /header/... │  │              │                  │
   │   └──────┬──────┘  └──────┬───────┘                  │
   │          │                │                          │
   │          └───────┬────────┘                          │
   │                  ▼                                    │
   │          HttpTransport.httpJson                        │
   │          (默认 transport = httpJson)                   │
   │                  │                                    │
   │                  ▼                                    │
   │               Route<Body, Prepared>                   │
   └───────────────────────────────────────────────────────┘

四轴的职责边界:

接口 回答的问题 示例
Protocol Protocol<Body,Frame,Event,State> 我说的是什么 API? OpenAI Chat / Anthropic Messages / Gemini / Bedrock Converse
Endpoint Endpoint<Body> 请求发到哪个 URL? baseURL + path(可为函数,URL 可嵌入 model id/region)
Auth Auth 怎么认证? bearer / header / none / custom,支持 orElse/andThen 组合
Framing Framing<Frame> 响应字节流怎么切成帧? sse(SSE data: 行)/ AWS event stream(二进制长度前缀帧)

2.3 Route.make 的两个重载

ts 复制代码
// 303:339:opencode/packages/llm/src/route/client.ts
export function make<Body, Prepared, Frame, Event, State>(
  input: MakeTransportInput<Body, Prepared, Frame, Event, State>,
): Route<Body, Prepared>
// ...
export function make<Body, Frame, Event, State>(
  input: MakeInput<Body, Frame, Event, State>,
): Route<Body, HttpTransport.HttpPrepared<Frame>>
export function make<Body, Prepared, Frame, Event, State>(
  input: MakeInput<Body, Frame, Event, State> | MakeTransportInput<Body, Prepared, Frame, Event, State>,
): Route<Body, Prepared> | Route<Body, HttpTransport.HttpPrepared<Frame>> {
  if ("transport" in input) return makeFromTransport(input)
  const protocol = input.protocol
  return makeFromTransport({
    id: input.id,
    provider: input.provider,
    protocol,
    endpoint: input.endpoint,
    auth: input.auth,
    headers: input.headers,
    transport: HttpTransport.httpJson({ framing: input.framing }),
    defaults: input.defaults,
  })
}

两个重载的区别在于第四轴的提供方式:

  • MakeInput(常用) :调用方只提供 framingmake 自动用 HttpTransport.httpJson({ framing }) 构造默认 HTTP transport。适用于绝大多数 HTTP+SSE 的 provider
  • MakeTransportInput(高级) :调用方直接提供完整 Transport<Body, Prepared, Frame>。适用于 WebSocket(如 OpenAI Responses WebSocket Route)或自定义传输

无论哪个重载,最终都走 makeFromTransport,保证两条路径的 build() 逻辑一致。

2.4 makeFromTransport:Route 的构建工厂

makeFromTransport 内部的 build() 函数完成了四轴到运行时方法的绑定:

ts 复制代码
// 247:301:opencode/packages/llm/src/route/client.ts
  const build = (routeInput: BuiltRouteInput): Route<Body, Prepared> => {
    const route: Route<Body, Prepared> = {
      id: routeInput.id,
      provider: routeInput.provider === undefined ? undefined : ProviderID.make(routeInput.provider),
      protocol: protocol.id,
      endpoint: routeInput.endpoint,
      auth: routeInput.auth ?? Auth.none,
      transport: routeInput.transport,
      defaults: routeInput.defaults ?? {},
      body: protocol.body,
      with: (patch: RoutePatch<Body, Prepared>) => { /* ... 深拷贝+合并 ... */ },
      model: (input) => makeRouteModel(route, input),
      prepareTransport: (body, request) =>
        routeInput.transport.prepare({ /* ... endpoint/auth/encodeBody/headers ... */ }),
      streamPrepared: (prepared, request, runtime) => {
        // frames → decode → step → LLMEvent
      },
    } satisfies Route<Body, Prepared>
    return route
  }

两个运行时方法的关键实现:

prepareTransport ------把 Body + LLMRequest 交给 transport 的 prepare,后者组装 URL(Endpoint.render)、叠加 headers、应用 Auth、JSON 编码 body,产出 Prepared(HTTP 场景是 HttpClientRequest + Framing)。

streamPrepared------完整的帧→事件翻译管道:

ts 复制代码
// 279:295:opencode/packages/llm/src/route/client.ts
      streamPrepared: (prepared: Prepared, request: LLMRequest, runtime: TransportRuntime) => {
        const route = `${request.model.provider}/${request.model.route.id}`
        const events = routeInput.transport
          .frames(prepared, request, runtime)
          .pipe(
            Stream.mapEffect(decodeEvent(route)),
            protocol.stream.terminal ? Stream.takeUntil(protocol.stream.terminal) : (stream) => stream,
          )
        return events.pipe(
          Stream.mapAccumEffect(
            () => protocol.stream.initial(request),
            protocol.stream.step,
            protocol.stream.onHalt ? { onHalt: protocol.stream.onHalt } : undefined,
          ),
          Stream.catchCause((cause) => Stream.fail(streamError(route, `Failed to read ${route} stream`, cause))),
        )
      },

管道三步:

  1. transport.frames() ------执行 HTTP 请求,把响应字节流经 Framing.frame 切成 Stream<Frame>
  2. Stream.mapEffect(decodeEvent) ------每帧用 protocol.stream.event schema 解码成 Event
  3. Stream.mapAccumEffect(initial, step) ------以 initial 为初始 State,逐个 Event 调 step,产出 Stream<LLMEvent>

decodeEventmakeFromTransport 顶部通过 Schema.decodeUnknownEffect(protocol.stream.event) 预编译一次,避免每个帧重复编译 schema。

2.5 DeepSeek/TogetherAI 如何复用 OpenAIChat.protocol

正交分离的直接收益是:provider 只需覆盖 Endpoint + Auth 两轴,Protocol 和 Framing 原样复用

第一步OpenAIChat.protocol 定义了 chat completions 的语义契约(body schema + streaming state machine):

ts 复制代码
// 481:493:opencode/packages/llm/src/protocols/openai-chat.ts
export const protocol = Protocol.make({
  id: ADAPTER,                    // "openai-chat"
  body: {
    schema: OpenAIChatBody,
    from: fromRequest,
  },
  stream: {
    event: Protocol.jsonEvent(OpenAIChatEvent),
    initial: () => ({ tools: ToolStream.empty(), toolCallEvents: [], lifecycle: Lifecycle.initial() }),
    step,
    onHalt: finishEvents,
  },
})

第二步OpenAICompatibleChat.route 原样复用这个 protocol,只改 route id 防止与原生 OpenAI 冲突:

ts 复制代码
// 17:22:opencode/packages/llm/src/protocols/openai-compatible-chat.ts
export const route = Route.make({
  id: ADAPTER,                    // "openai-compatible-chat"
  protocol: OpenAIChat.protocol,  // ← 原样复用,零改动
  endpoint: Endpoint.path("/chat/completions"),
  framing: Framing.sse,
})

第三步openai-compatible.tsdefine(profile) 工厂为每个 provider 覆盖 Endpoint(baseURL)+ Auth(bearer),生成专属 Route:

ts 复制代码
// 38:52:opencode/packages/llm/src/providers/openai-compatible.ts
const define = (profile: OpenAICompatibleProfile) => {
  const configureProfile = (input: FamilyModelOptions = {}) => {
    const facade = configure({
      ...input,
      baseURL: input.baseURL ?? profile.baseURL,   // ← 只改 baseURL
      provider: profile.provider,
    })
    return {
      id: ProviderID.make(profile.provider),
      model: facade.model,
      configure: configureProfile,
    }
  }
  return configureProfile()
}

export const deepseek = define(profiles.deepseek)     // baseURL = "https://api.deepseek.com/v1"
export const togetherai = define(profiles.togetherai)  // baseURL = "https://api.together.xyz/v1"
export const cerebras = define(profiles.cerebras)       // baseURL = "https://api.cerebras.ai/v1"

configure 内部通过 route.with({ endpoint: { baseURL }, auth: AuthOptions.bearer(...) }) patch 一个新 Route------因为 with 是不可变拷贝,原 OpenAICompatibleChat.route 不受影响,每个 provider 拿到自己绑定的 Route 实例。

复用度量 :DeepSeek/TogetherAI/Cerebras/Groq/Fireworks/DeepInfra/Baseten 七个 provider 共享同一份 OpenAIChat.protocol(~500 行 body schema + lowering + stream parser),每个 provider 只需 3 行 define(profile)。如果没有四轴正交,每个 provider 要复制全部 500 行------这正是 protocol.ts 注释所说的"This separation is what lets DeepSeek, TogetherAI, Cerebras, etc. all reuse OpenAIChat.protocol without forking 300 lines per provider"。

三、compile():编译边界

compileLLMClient 的核心内部函数,是公共 LLMRequest 与 provider 原生世界之间的编译边界------它把请求编译成"已校验的 body + 已预备的 transport 数据",但不执行传输。

3.1 compile 的四步流水线

ts 复制代码
// 344:359:opencode/packages/llm/src/route/client.ts
const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) {
  const resolved = applyCachePolicy(resolveRequestOptions(request))
  const route = resolved.model.route

  const body = yield* route.body
    .from(resolved)
    .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema))))
  const prepared = yield* route.prepareTransport(body, resolved)

  return {
    request: resolved,
    route,
    body,
    prepared,
  }
})

四步:

  1. resolveRequestOptions(request)------合并三层默认值(route defaults → model defaults → request 字段),产出 resolved request
  2. applyCachePolicy(resolved)------根据 cache policy 注入 inline cache hints(详见第四节)
  3. route.body.from(resolved)Schema.decodeUnknown(body.schema) ------调用 protocol 的 lowering 函数构建 provider body,然后用 body schema 校验它。ProviderShared.validateWith 包装了 decode,失败时产出 InvalidProviderOutput 类型化错误
  4. route.prepareTransport(body, resolved) ------把已校验的 body + request 交给 transport 的 prepare,产出 transport 私有的 Prepared 数据(HTTP 场景是组装好的 HttpClientRequest + Framing

3.2 resolveRequestOptions:三层默认值合并

ts 复制代码
// 167:180:opencode/packages/llm/src/route/client.ts
const resolveRequestOptions = (request: LLMRequest) => {
  const routeDefaults = request.model.route.defaults
  const modelDefaults = request.model.defaults
  const generation = mergeGenerationOptions(routeDefaults.generation, modelDefaults?.generation, request.generation)
  return LLMRequest.update(request, {
    generation: generation ?? new GenerationOptions({}),
    providerOptions: mergeProviderOptions(
      routeDefaults.providerOptions,
      modelDefaults?.providerOptions,
      request.providerOptions,
    ),
    http: mergeHttpOptions(routeDefaults.http, modelDefaults?.http, request.http),
  })
}

三层优先级从低到高route.defaults(协议级默认)→ model.defaults(模型级默认)→ request.*(请求级显式)。mergeGenerationOptionsfindLast 取最后一个非 undefined 值(后者覆盖前者),mergeProviderOptions/mergeHttpOptions 做深合并(JSON object 递归合并)。

3.3 编译边界的设计含义

compile 是"编译"而非"执行",它有三个关键设计约束:

约束一:编译不执行传输compile 产出 { request, route, body, prepared } 四元组,但 prepared 只是 transport 的预备数据(HTTP 是 HttpClientRequest 对象),网络请求尚未发出。这让 LLMClient.prepare() 可以安全地暴露给调试 UI 和请求预览------用户可以看到"即将发什么"而不实际发送。

约束二:body schema 是编译期闸门body.from 产出候选 body 后,Schema.decodeUnknown(body.schema) 立即校验。这意味着 protocol 实现者的 lowering 逻辑如果有 bug(如遗漏 required 字段、类型不匹配),会在 compile 阶段就失败,而不是等请求发出去后才在 provider 端报 400。错误类型是 InvalidProviderOutputReason------"provider 输出无效",因为这是 protocol 自己产出的 body 不合法。

约束三:cache policy 在 body 构建前注入applyCachePolicyresolveRequestOptions 之后、body.from 之前执行,这样 lowering 函数拿到的 resolved request 已经带了 cache hints,可以在构建 body 时自然地把 hints 翻译为 wire markers(如 Anthropic 的 cache_control)。这是一个"在正确位置注入,让后续自然处理"的设计模式。

3.4 prepare 与 stream 如何消费 compile 的产出

compile 的产出被三个公共方法消费:

ts 复制代码
// 361:408:opencode/packages/llm/src/route/client.ts
const prepareWith = Effect.fn("LLMClient.prepare")(function* (request: LLMRequest) {
  const compiled = yield* compile(request)
  return new PreparedRequest({
    id: compiled.request.id ?? "request",
    route: compiled.route.id,
    protocol: compiled.route.protocol,
    model: compiled.request.model,
    body: compiled.body,
    metadata: { transport: compiled.route.transport.id },
  })
})

const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) =>
  Stream.unwrap(
    Effect.gen(function* () {
      const compiled = yield* compile(request)
      return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime)
    }),
  )

const generateWith = (stream: Interface["stream"]) =>
  Effect.fn("LLM.generate")(function* (request: LLMRequest) {
    const state = yield* stream(request).pipe(Stream.runFold(LLMResponse.empty, LLMResponse.reduce))
    const response = LLMResponse.complete(state)
    if (response) return response
    return yield* ProviderShared.eventError(/* ... "stream ended without a terminal finish event" */)
  })
  • prepare:只调 compile,不执行。返回 PreparedRequest(包含 body + route/protocol/model 元数据)
  • stream:调 compile 后用 Stream.unwrap 把 Effect 转为 Stream,然后执行 streamPrepared(含传输 + 分帧 + 解码 + 状态机)。Stream.unwrap 让 compile 的 Effect 在 Stream 首次拉取时执行,实现 lazy 语义
  • generate:在 stream 基础上用 Stream.runFold 归约成 LLMResponse。归约器是 LLMResponse.reduce(定义在 schema/events.ts),它把事件流组装成 Message + usage + finishReason

四、cache-policy.ts:inline hint 注入

applyCachePolicy 是 compile 的第一步,负责根据请求的 cache 策略自动在正确的 part 上注入 CacheHint,让后续的 lowering 函数自然处理。

4.1 为什么只对 anthropic-messages 和 bedrock-converse 注入

ts 复制代码
// 39:42:opencode/packages/llm/src/cache-policy.ts
// Protocols whose wire format ignores inline cache markers (OpenAI's implicit
// prefix caching, Gemini's implicit + out-of-band CachedContent). Skip the
// whole policy pass for these --- emitting hints would be harmless but pointless.
const RESPECTS_INLINE_HINTS = new Set(["anthropic-messages", "bedrock-converse"])
ts 复制代码
// 99:101:opencode/packages/llm/src/cache-policy.ts
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
  if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
  // ...
}

原因在于不同 provider 的缓存机制不同

Provider 缓存机制 是否尊重 inline hint
Anthropic Messages 显式 cache_control: { type: "ephemeral" } 标记在 content block 上 ✅ 是------wire format 有 inline marker 字段
Bedrock Converse 显式 cachePoint 标记在 body 中 ✅ 是------wire format 有 inline marker
OpenAI Chat/Responses 隐式 prefix caching------自动缓存请求前缀,无需 marker ❌ 否------wire format 无 marker 字段
Gemini 隐式 + out-of-band CachedContent API ❌ 否------用独立 API 管理缓存

对 OpenAI/Gemini 注入 hints 是"无害但无意义的"(harmless but pointless)------lowering 函数会忽略 CacheHint 字段。因此 applyCachePolicy 在第一行就短路返回,跳过整个策略 pass,避免对消息数组做无谓的遍历和拷贝。

4.2 auto 策略的三个断点

ts 复制代码
// 18:23:opencode/packages/llm/src/cache-policy.ts
const AUTO: CachePolicyObject = {
  tools: true,
  system: true,
  messages: "latest-user-message",
}

默认 "auto" 策略在三个位置放置 cache 断点:

  1. tools: true------最后一个 tool definition 上。tools 在缓存层级中最高(tool 列表变化频率最低)
  2. system: true------最后一个 system part 上。system prompt 在一次会话中通常不变
  3. messages: "latest-user-message"------最近一条 user message 的最后一个 text part 上

这个设计的经济学依据在注释中写明:Anthropic 5 分钟缓存的写入成本是 1.25x base,读取成本是 0.1x base------单次复用即已盈利。在 Agent 循环中,一次 user message 会触发多轮 assistant/tool 来回,这些来回都共享同一前缀,在 "latest user message" 处设断点让每一轮的 intra-turn API call 都命中缓存。

4.3 策略解析与断点注入

ts 复制代码
// 33:37:opencode/packages/llm/src/cache-policy.ts
const resolve = (policy: CachePolicy | undefined): CachePolicyObject => {
  if (policy === undefined || policy === "auto") return AUTO
  if (policy === "none") return NONE
  return policy
}
ts 复制代码
// 104:111:opencode/packages/llm/src/cache-policy.ts
  const hint = makeHint(policy.ttlSeconds)
  const tools = policy.tools ? markLastTool(request.tools, hint) : request.tools
  const system = policy.system ? markLastSystem(request.system, hint) : request.system
  const messages = policy.messages ? markMessages(request.messages, policy.messages, hint) : request.messages

  if (tools === request.tools && system === request.system && messages === request.messages) return request
  return LLMRequest.update(request, { tools, system, messages })

解析规则:

  • undefined"auto"(默认开启缓存,数学上划算)
  • "auto" → tools + system + latest-user-message
  • "none" → 不做自动放置(但手动 CacheHint 仍然流转)
  • 对象形式 → 精确按调用方指定的位置

注入逻辑尊重手动放置------markLastTool/markLastSystem/markMessageAt 都检查目标 part 是否已有 cache 字段,有则跳过。这保证用户的显式控制不被覆盖。

消息策略的三种模式

ts 复制代码
// 85:97:opencode/packages/llm/src/cache-policy.ts
const markMessages = (
  messages: ReadonlyArray<Message>,
  strategy: NonNullable<CachePolicyObject["messages"]>,
  hint: CacheHint,
): ReadonlyArray<Message> => {
  if (messages.length === 0) return messages
  if (strategy === "latest-user-message") return markMessageAt(messages, lastIndexOfRole(messages, "user"), hint)
  if (strategy === "latest-assistant") return markMessageAt(messages, lastIndexOfRole(messages, "assistant"), hint)
  const start = Math.max(0, messages.length - strategy.tail)
  let next = messages
  for (let i = start; i < messages.length; i++) next = markMessageAt(next, i, hint)
  return next
}
  • "latest-user-message":在最后一条 user message 的最后一个 text part 上标记
  • "latest-assistant":在最后一条 assistant message 上标记
  • { tail: N }:在最后 N 条 message 上各标记一个断点

4.4 注入后的流转路径

applyCachePolicy 注入 CacheHint 后,hints 通过 LLMRequest 的 part 字段流转到 protocol 的 lowering 函数。以 Anthropic 为例:

ts 复制代码
// 243:251:opencode/packages/llm/src/protocols/anthropic-messages.ts
const cacheControl = (breakpoints: Cache.Breakpoints, cache: CacheHint | undefined) => {
  if (cache?.type !== "ephemeral" && cache?.type !== "persistent") return undefined
  if (breakpoints.remaining <= 0) {
    breakpoints.dropped += 1
    return undefined
  }
  breakpoints.remaining -= 1
  return Cache.ttlBucket(cache.ttlSeconds) === "1h" ? EPHEMERAL_1H : EPHEMERAL_5M
}

Anthropic 的 lowering 函数读取每个 part 的 cache 字段,通过 cacheControl 翻译为 wire 格式的 cache_control: { type: "ephemeral", ttl: "5m"|"1h" }。同时管理 Anthropic 的 4 断点上限 ------ANTHROPIC_BREAKPOINT_CAP = 4,超过时按 tools → system → messages 的失效顺序丢弃(tools 层级最高优先保留),并 log warning。

这样 applyCachePolicy(protocol 无关)+ cacheControl(protocol 特定)形成两层:第一层在编译边界注入语义 hints,第二层在 lowering 时翻译为 wire markers 并管理 provider 限制。

五、executor.ts:类型化错误映射与预输出重试

RequestExecutor 是 LLM 网关的 HTTP 传输层。它把 Effect 的 HttpClient 包裹成类型化的 LLMError 产出器,并实现"仅预输出重试"(pre-output retry)------只在流开始前重试,流开始后不再重试。

5.1 类型化错误体系

executor.ts 不自己定义错误类型,而是消费 schema/errors.ts 中定义的 10 种 LLMErrorReason。每种 Reason 都有 retryable getter 标记是否可重试:

ts 复制代码
// 160:172:opencode/packages/llm/src/schema/errors.ts
export const LLMErrorReason = Schema.Union([
  InvalidRequestReason,      // 400/404/409/413/422 --- retryable: false
  NoRouteReason,             // 无路由 --- retryable: false
  AuthenticationReason,     // 401/403 --- retryable: false
  RateLimitReason,          // 429 --- retryable: true
  QuotaExceededReason,      // 429 + quota --- retryable: false
  ContentPolicyReason,      // content_filter --- retryable: false
  ProviderInternalReason,    // 5xx --- retryable: true
  TransportReason,          // 网络层 --- retryable: false
  InvalidProviderOutputReason, // provider 输出无效 --- retryable: false
  UnknownProviderReason,    // 未知状态 --- retryable: false
]).pipe(Schema.toTaggedUnion("_tag"))
ts 复制代码
// 174:192:opencode/packages/llm/src/schema/errors.ts
export class LLMError extends Schema.TaggedErrorClass<LLMError>()("LLM.Error", {
  module: Schema.String,
  method: Schema.String,
  reason: LLMErrorReason,
}) {
  override readonly cause = this.reason

  get retryable() {
    return this.reason.retryable
  }

  get retryAfterMs() {
    return "retryAfterMs" in this.reason ? this.reason.retryAfterMs : undefined
  }
  // ...
}

LLMErrorTaggedErrorClass,其 retryableretryAfterMs 代理到 reason。这让 executor 的重试逻辑可以统一查询 error.retryable 而不需要 switch reason 类型。

5.2 HTTP 状态码到 Reason 的映射

statusReason 是映射的核心------输入 HTTP 状态码 + 响应体,输出对应的 LLMErrorReason

ts 复制代码
// 225:275:opencode/packages/llm/src/route/executor.ts
const statusReason = (input: {
  readonly status: number
  readonly message: string
  readonly retryAfterMs?: number | undefined
  readonly rateLimit?: HttpRateLimitDetails | undefined
  readonly http: HttpContext
}) => {
  const body = input.http.body ?? ""
  if (/content[-_\s]?policy|content_filter|safety/i.test(body)) {
    return new ContentPolicyReason({ message: input.message, http: input.http })
  }
  if (input.status === 401) {
    return new AuthenticationReason({ message: input.message, kind: "invalid", http: input.http })
  }
  if (input.status === 403) {
    return new AuthenticationReason({ message: input.message, kind: "insufficient-permissions", http: input.http })
  }
  if (input.status === 429) {
    if (/insufficient[-_\s]?quota|quota[-_\s]?exceeded/i.test(body)) {
      return new QuotaExceededReason({ message: input.message, http: input.http })
    }
    return new RateLimitReason({
      message: input.message,
      retryAfterMs: input.retryAfterMs,
      rateLimit: input.rateLimit,
      http: input.http,
    })
  }
  if (
    input.status === 400 ||
    input.status === 404 ||
    input.status === 409 ||
    input.status === 413 ||
    input.status === 422
  ) {
    return new InvalidRequestReason({
      message: input.message,
      classification: isContextOverflow(body) ? "context-overflow" : undefined,
      http: input.http,
    })
  }
  if (input.status >= 500 || retryableStatus(input.status)) {
    return new ProviderInternalReason({
      message: input.message,
      status: input.status,
      retryAfterMs: input.retryAfterMs,
      http: input.http,
    })
  }
  return new UnknownProviderReason({ message: input.message, status: input.status, http: input.http })
}

映射逻辑按优先级排列:

HTTP 状态码 Reason retryable 特殊逻辑
任何 + body 含 content_policy/safety ContentPolicyReason false body 正则匹配优先于状态码
401 AuthenticationReason (kind="invalid") false ---
403 AuthenticationReason (kind="insufficient-permissions") false ---
429 + body 含 quota_exceeded QuotaExceededReason false 配额耗尽不可重试
429 (其他) RateLimitReason true 携带 retryAfterMs + rateLimit 详情
400/404/409/413/422 InvalidRequestReason false 检测 isContextOverflow(body) → classification="context-overflow"
500+ 或 429/503/504/529 ProviderInternalReason true 携带 retryAfterMs
其他 UnknownProviderReason false 兜底

两个值得注意的设计

  1. ContentPolicy 优先于状态码 :即使状态码是 400,如果 body 含 content_filter/safety,优先归类为 ContentPolicyReason。这让上层可以根据"内容政策"做差异化处理(如提示用户修改 prompt 而非重试)
  2. Context overflow 检测 :400/413 等错误会检查 body 是否匹配 context overflow 的正则模式(isContextOverflow)。匹配则给 InvalidRequestReasonclassification: "context-overflow",这让上层(如 core 的 compaction 逻辑)可以精确检测"上下文溢出"而非通用"无效请求"

5.3 context overflow 的模式匹配

provider-error.ts 定义了 22 个正则模式匹配各 provider 的 context overflow 消息:

ts 复制代码
// 4:28:opencode/packages/llm/src/provider-error.ts
const patterns = [
  /prompt is too long/i,
  /input is too long for requested model/i,
  /exceeds the context window/i,
  /input token count.*exceeds the maximum/i,
  /tokens in request more than max tokens allowed/i,
  // ... 共 22 个模式 ...
  /model_context_window_exceeded/i,
]

export const isContextOverflow = (message: string) =>
  patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)

export const isContextOverflowFailure = (failure: unknown) =>
  failure instanceof LLMError
    ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow"
    : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow"

isContextOverflow 覆盖了 Anthropic(prompt is too long)、OpenAI(maximum context length)、Gemini、Bedrock 等各 provider 的不同措辞。isContextOverflowFailure 则用于在流式事件中检测 ProviderErrorEvent------因为 context overflow 也可能在流中途发生(provider 先开始流再发现超长)。

5.4 传输层错误的归一化

HTTP 传输层(DNS/连接/超时)的错误也需要归一化为 LLMErrortoHttpError 把 Effect HttpClientError 转为 TransportReason

ts 复制代码
// 307:343:opencode/packages/llm/src/route/executor.ts
const toHttpError = (redactedNames: ReadonlyArray<string | RegExp>) => (error: unknown) => {
  const transportError = (input: { /* ... */ }) =>
    new LLMError({
      module: "RequestExecutor",
      method: "execute",
      reason: new TransportReason({
        message: input.message,
        kind: input.kind,
        url: input.request ? redactUrl(input.request.url) : undefined,
        http: input.request ? new HttpContext({ request: requestDetails(input.request, redactedNames) }) : undefined,
      }),
    })

  if (Cause.isTimeoutError(error)) {
    return transportError({ message: error.message, kind: "Timeout" })
  }
  if (!HttpClientError.isHttpClientError(error)) {
    return transportError({ message: "HTTP transport failed" })
  }
  const request = "request" in error ? error.request : undefined
  if (error.reason._tag === "TransportError") {
    return transportError({
      message: error.reason.description ?? "HTTP transport failed",
      kind: error.reason._tag,
      request,
    })
  }
  return transportError({
    message: `HTTP transport failed: ${error.reason._tag}`,
    kind: error.reason._tag,
    request,
  })
}

TransportReasonretryable 为 false------传输层错误不自动重试(因为可能是 DNS/证书等持久性故障)。但超时(Cause.isTimeoutError)有专门的 kind: "Timeout" 标记,上层可以据此判断是否值得重试。

5.5 预输出重试策略

ts 复制代码
// 345:364:opencode/packages/llm/src/route/executor.ts
const retryDelay = (error: LLMError, attempt: number) => {
  if (error.retryAfterMs !== undefined) return Effect.succeed(Math.min(error.retryAfterMs, MAX_DELAY_MS))
  return Random.nextBetween(
    Math.min(BASE_DELAY_MS * 2 ** attempt * 0.8, MAX_DELAY_MS),
    Math.min(BASE_DELAY_MS * 2 ** attempt * 1.2, MAX_DELAY_MS),
  ).pipe(Effect.map((delay) => Math.round(delay)))
}

const retryStatusFailures = <A, R>(
  effect: Effect.Effect<A, LLMError, R>,
  retries = MAX_RETRIES,
  attempt = 0,
): Effect.Effect<A, LLMError, R> =>
  Effect.catchTag(effect, "LLM.Error", (error): Effect.Effect<A, LLMError, R> => {
    if (!error.retryable || retries <= 0) return Effect.fail(error)
    return retryDelay(error, attempt).pipe(
      Effect.flatMap((delay) => Effect.sleep(delay)),
      Effect.flatMap(() => retryStatusFailures(effect, retries - 1, attempt + 1)),
    )
  })

重试参数:MAX_RETRIES = 2(最多重试 2 次,共 3 次请求)、BASE_DELAY_MS = 500MAX_DELAY_MS = 10_000

退避策略

  • 如果错误携带 retryAfterMs(如 RateLimitReasonRetry-After header 解析),优先使用它,但封顶 MAX_DELAY_MS
  • 否则用指数退避 + 抖动BASE_DELAY_MS * 2^attempt,乘以 0.8~1.2 的随机抖动,封顶 MAX_DELAY_MS。抖动避免多个客户端同步重试(thundering herd)

"仅预输出重试"的语义

ts 复制代码
// 366:380:opencode/packages/llm/src/route/executor.ts
export const layer: Layer.Layer<Service, never, HttpClient.HttpClient> = Layer.effect(
  Service,
  Effect.gen(function* () {
    const http = yield* HttpClient.HttpClient
    const executeOnce = (request: HttpClientRequest.HttpClientRequest) =>
      Effect.gen(function* () {
        const redactedNames = yield* Headers.CurrentRedactedNames
        return yield* http
          .execute(request)
          .pipe(Effect.mapError(toHttpError(redactedNames)), Effect.flatMap(statusError(request, redactedNames)))
      })
    return Service.of({
      execute: (request) => retryStatusFailures(executeOnce(request)),
    })
  }),
)

retryStatusFailures 包裹的是 executeOnce------即一次完整的 HTTP 请求 (发请求 + 读状态码 + 解析 body + 映射为 LLMError 或返回 response)。重试发生在这个层面:如果 executeOnce 失败(返回 LLMError),且 error.retryable 为 true,则延迟后重试整个请求。

关键设计 :重试只在"获取 HTTP Response"阶段发生。一旦 executeOnce 成功返回 response(status < 400),retryStatusFailures 就结束了------后续的 transport.frames() 读取响应流不再被重试包裹。这意味着:

  • ✅ 请求被 rate limit → 自动重试(因为 executeOnce 失败)
  • ✅ 请求 503 → 自动重试(因为 executeOnce 失败)
  • ❌ 流开始后 provider 中途断开 → 不重试(因为已经过了 executeOnce,进入 transport.frames()
  • ❌ 流中途的 provider-error 事件 → 不重试(因为这是 stream 层面的 LLMEvent,不是 LLMError)

这个设计是正确的------流已经开始后重试会导致重复输出(用户已经看到部分文本),而预输出阶段的重试对用户透明(什么都没输出过)。

5.6 敏感信息脱敏

executor 还负责错误中的敏感信息脱敏,确保 LLMErrorHttpContext 不会泄漏 API key:

ts 复制代码
// 48:54:opencode/packages/llm/src/route/executor.ts
const SENSITIVE_NAME_SOURCE =
  "authorization|api[-_]?key|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|credential|signature|x-amz-signature"
const SENSITIVE_NAME = new RegExp(SENSITIVE_NAME_SOURCE, "i")
const SHORT_QUERY_NAME = /^(key|sig)$/i
const SENSITIVE_BODY_FIELD = new RegExp(`(?:${SENSITIVE_NAME_SOURCE}|key)`, "i")
const REDACT_JSON_FIELD = new RegExp(`("(?:${SENSITIVE_BODY_FIELD.source})"\\s*:\\s*)"[^"]*"`, "gi")
const REDACT_QUERY_FIELD = new RegExp(`((?:${SENSITIVE_BODY_FIELD.source})=)[^&\\s"]+`, "gi")

脱敏分两层:

  1. 结构脱敏 :用正则匹配 body/query 中所有看起来像敏感字段名(authorization/token/secret/key 等)的 JSON 字段和 query 参数,替换为 <redacted>
  2. 字面脱敏 :收集请求中实际发送的 secret 值(header 里的 Bearer token、query 里的 ?key=...),在响应 body 中搜索替换------防止 provider 把 secret 回显到错误消息中

URL 中的敏感 query 参数也被脱敏(redactUrl),request-id 提取支持 5 种 header 变体(x-request-id/request-id/x-amzn-requestid/x-amz-request-id/x-goog-request-id/cf-ray),兼容各 provider 的 request id 惯例。

5.7 Rate Limit 详情的解析

ts 复制代码
// 112:148:opencode/packages/llm/src/route/executor.ts
const rateLimitDetails = (headers: Record<string, string>, retryAfter: number | undefined) => {
  const limit: Record<string, string> = {}
  const remaining: Record<string, string> = {}
  const reset: Record<string, string> = {}

  Object.entries(headers).forEach(([name, value]) => {
    const openaiLimit = /^x-ratelimit-limit-(.+)$/.exec(name)?.[1]
    if (openaiLimit) return addRateLimitValue(limit, openaiLimit, value)

    const openaiRemaining = /^x-ratelimit-remaining-(.+)$/.exec(name)?.[1]
    if (openaiRemaining) return addRateLimitValue(remaining, openaiRemaining, value)

    const openaiReset = /^x-ratelimit-reset-(.+)$/.exec(name)?.[1]
    if (openaiReset) return addRateLimitValue(reset, openaiReset, value)

    const anthropic = /^anthropic-ratelimit-(.+)-(limit|remaining|reset)$/.exec(name)
    if (!anthropic) return
    if (anthropic[2] === "limit") return addRateLimitValue(limit, anthropic[1], value)
    if (anthropic[2] === "remaining") return addRateLimitValue(remaining, anthropic[1], value)
    return addRateLimitValue(reset, anthropic[1], value)
  })
  // ...
  return new HttpRateLimitDetails({ retryAfterMs: retryAfter, limit, remaining, reset })
}

rateLimitDetails 解析两种 rate limit header 风格:

  • OpenAI 风格x-ratelimit-limit-requestsx-ratelimit-remaining-tokensx-ratelimit-reset-requests
  • Anthropic 风格anthropic-ratelimit-requests-limitanthropic-ratelimit-requests-remaininganthropic-ratelimit-requests-reset

两种风格的 limit/remaining/reset 按 dimension(requests/tokens)分组存入 HttpRateLimitDetails,附带 retryAfterMs(从 Retry-After/retry-after-ms header 解析)。这些详情附加到 RateLimitReason 上,让上层可以精确展示"还剩多少 quota、何时重置"。

六、LLM 网关的类型化设计范式

6.1 设计范式总结

OpenCode 的 LLM 网关体现了一个完整的类型化设计范式,可以概括为五层:

bash 复制代码
┌──────────────────────────────────────────────────────────────┐
│  第五层:类型化错误(10 种 LLMErrorReason + retryable getter)│
│  schema/errors.ts --- 每种错误自带可重试性 + HTTP 上下文       │
└───────────────────────────┬──────────────────────────────────┘
                            │ statusReason() 映射
┌───────────────────────────▼──────────────────────────────────┐
│  第四层:传输执行(executor.ts --- HTTP + 重试 + 脱敏)        │
│  预输出重试(MAX_RETRIES=2)+ 敏感信息脱敏 + rate limit 解析  │
└───────────────────────────┬──────────────────────────────────┘
                            │ transport.frames() + streamPrepared()
┌───────────────────────────▼──────────────────────────────────┐
│  第三层:编译边界(compile --- cache policy + body + schema)  │
│  resolveRequestOptions → applyCachePolicy → body.from →       │
│  schema.decode → prepareTransport                             │
└───────────────────────────┬──────────────────────────────────┘
                            │ Route.make() 四轴组合
┌───────────────────────────▼──────────────────────────────────┐
│  第二层:部署组合(Route = Protocol + Endpoint + Auth +      │
│  Framing/Transport)--- 四轴正交,DeepSeek 只改 Endpoint+Auth  │
└───────────────────────────┬──────────────────────────────────┘
                            │ Protocol.make()
┌───────────────────────────▼──────────────────────────────────┐
│  第一层:语义契约(Protocol<Body,Frame,Event,State>)        │
│  body.schema + body.from + stream.event + stream.step         │
└──────────────────────────────────────────────────────────────┘

6.2 关键设计不变量

不变量 位置 约束
Protocol ≠ 部署 protocol.ts:20-24 Protocol 不知道 URL、header、auth scheme------这些是 Route.make 的部署关切
四轴正交 client.ts:321-339 Protocol/Endpoint/Auth/Framing 独立可替换,DeepSeek/TogetherAI 只覆盖 Endpoint+Auth
编译不执行 client.ts:344-359 compile 产出 { body, prepared } 但不发送网络请求------prepare() 安全暴露给调试
body schema 是闸门 client.ts:348-350 body.from 后立即 schema.decodeUnknown 校验,lowering bug 在 compile 阶段就失败
cache policy 先于 lowering client.ts:345 applyCachePolicy 在 body.from 之前注入 hints,让 lowering 自然翻译为 wire markers
inline hints 仅 Anthropic/Bedrock cache-policy.ts:42 OpenAI/Gemini 隐式缓存,注入 hints 无害但无意义,短路跳过
错误类型化 errors.ts:160-192 10 种 Reason 各有 retryable getter,executor 统一查询不 switch
预输出重试 executor.ts:353-364 重试只包裹 executeOnce(获取 response),流开始后不重试
敏感信息脱敏 executor.ts:48-55 结构脱敏 + 字面脱敏双层,防 provider 回显 secret
context overflow 检测 provider-error.ts:4-28 22 个正则覆盖各 provider 措辞,classification="context-overflow"

6.3 与 AI SDK 的对比

OpenCode 的 LLM 网关设计相比 Vercel AI SDK(@ai-sdk/*core 依赖 18 个 @ai-sdk/* provider)有几个关键差异:

维度 OpenCode @opencode-ai/llm Vercel AI SDK
协议抽象 Protocol<Body,Frame,Event,State> 四类型参数管道 无显式 Protocol 类型,每个 provider 自己实现
部署正交 Route.make 四轴正交,DeepSeek 3 行接入 每个 @ai-sdk/* 包独立实现,无共享 protocol
请求编译 compile() 显式编译边界(schema 校验 + transport 预备) 无编译概念,直接 streamText
缓存策略 cache-policy.ts 协议无关 auto 注入 各 provider 各自处理,无统一策略
错误类型化 10 种 LLMErrorReason + retryable getter ProviderError 无细分 reason
重试策略 预输出重试(只在获取 response 阶段) 通常无内置重试或各 provider 自行实现

OpenCode 的设计更重、更显式,但换来的是:新增 provider 的边际成本极低 (只需 Endpoint+Auth patch)、错误处理可类型化决策 (上层可以 if (error.reason._tag === "RateLimit") 做不同策略)、缓存策略统一管理(agent 循环自动获得 prompt caching 收益)。

6.4 设计哲学

  1. Protocol 是"API 语义"的单一来源:一个 protocol 定义了"这个 API 长什么样"的全部知识------body 怎么构建、stream 怎么解析。部署(URL/auth/transport)是正交的、可替换的维度。这让 protocol 成为可复用的语义资产,而非一次性的 provider 实现
  2. compile 是"类型安全"的编译边界:在 body.from 和 transport 之间插入 schema 校验,让 protocol 实现者的 lowering bug 在编译期(而非运行时 400 错误)就被捕获。compile 不执行的设计让请求预览成为免费的副产品
  3. cache policy 是"协议无关"的策略层:auto 策略在编译边界注入语义 hints,protocol 的 lowering 自然翻译为 wire markers。两层分离让缓存策略可以统一演进而不动每个 protocol
  4. 错误是"类型化决策"的输入:10 种 Reason + retryable getter 让上层可以用类型 switch 做不同策略(context-overflow → compaction,rate-limit → 退避重试,authentication → 提示用户)。executor 的预输出重试是这一层的默认消费者
  5. 正交组合优于继承 :DeepSeek 不继承 OpenAIChat,而是 OpenAIChat.protocol + DeepSeek.endpoint + bearer.auth。这避免了继承层次的爆炸(M×N 个组合),用 M+N 个正交维度覆盖所有组合
相关推荐
JJJennie77741 分钟前
当AI开始学会欺骗
网络·人工智能
糖果店的幽灵1 小时前
大模型测评DeepEval快速入门-安全与通用指标详解
人工智能·安全·langgraph·大模型测评·deepeval
江畔柳前堤7 小时前
roLabelImg 详细安装教程
开发语言·人工智能·后端·云原生
阿里云大数据AI技术7 小时前
分链路差异化设计的DSP准实时数仓|钛动科技基于阿里云实时计算 Flink 版 + DLF Paimon + EMR Serverless StarRocks 的实践
人工智能·flink
陕西企来客7 小时前
2026年7月AI智能搜索曝光趋势研判
大数据·人工智能·机器学习·ai智能搜索曝光
阿里云大数据AI技术8 小时前
从算力到智能体,面向 Agentic AI 的基础设施演进
人工智能·agent
hangyuekejiGEO8 小时前
GEO技术服务选型指南
大数据·人工智能·python
阿里云大数据AI技术8 小时前
EMR Serverless Spark AI Function 的双维降本实践
人工智能·sql·spark
维基框架9 小时前
GitHub源码处理提速 一趟扫描反而更慢
人工智能·github
冬奇Lab9 小时前
代码库知识库系列(05):向量检索 vs 知识图谱——加了调用图并没有变更好
人工智能