Skip to content

OpenAI-compatible Provider

本课交付结果

你将交付 OpenAICompatibleProvider.complete:构造 chat/completions 请求,把 response 映射为 message/tool/finish Action 与 usage;缺 key 预检,429/5xx 有界重试,timeout/取消使用 AbortSignal,错误中脱敏 key。

岗位问题

Provider 是不稳定外部协议的隔离层。上游字段、HTTP 状态和错误正文不能直接泄漏进 Agent Core;API key 更不能出现在错误。通过注入 fetch,所有协议分支可以在无网络测试中确定复现。

前置检查

前置知识快照

先完成 Fake Provider 与 Action Protocol:

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 03
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 04

本 Lab 的 baseUrl 使用 .invalid,唯一 fetch 来源是测试注入函数。

第 3 课 Fake Provider 给 Agent Core 一个确定 ModelProvider,第 4 课定义 message/tool/finish Action。真实 Provider 的职责是把不稳定外部 HTTP schema 收敛到这份内部 contract:Core 不认识 choices、finish_reason、prompt_tokens 或某厂商 error body,只看到经过验证的 action、usage 和稳定错误。

OpenAI-compatible 表示共享大致接口形状,不代表每个服务字段、工具调用、流式事件和错误完全相同。适配器要保守验证实际响应,用契约 fixture 覆盖目标服务差异;不能用 any 一路传进 Core。baseUrl、model、timeout、retry 与 credential 在构造时校验,fetch 作为依赖注入,测试全程不访问网络。

API key 缺失与外部 signal 已取消都在 fetch 前拒绝,spy 调用为零。key 只进入 Authorization header,不进入 body、config 打印、Trace 和错误。自定义 baseUrl 还需 CLI 的 host 信任策略,避免把标准 key 发往项目配置的陌生服务。

原理拆解

mermaid
flowchart TD
  A[ModelRequest] --> B[预检 key/config/signal]
  B --> C[映射 messages/tools]
  C --> D[fetch + timeout AbortController]
  D --> E{HTTP 状态}
  E -->|429/指定 5xx| F[有界退避重试]
  E -->|其他错误| G[安全 ProviderError]
  E -->|成功| H[严格解析 response]
  H --> I{tool_calls?}
  I -->|是| J[Tool Action]
  I -->|否且 content| K[Message Action]
  I -->|否则| L[Finish Action]
  J --> M[归一 usage]
  K --> M
  L --> M

响应优先检查 tool_calls,再检查非空 content,最后映射 finish_reason。usage 将 prompt_tokens/completion_tokens 归一为 inputTokens/outputTokens。只有 429 与 500/502/503/504 可重试,次数严格受 maxRetries 限制。

代码实验

打开本课 Lab

bash
pnpm --dir bootcamps/coding-agent/labs/37-openai-compatible-provider/starter test
pnpm --dir bootcamps/coding-agent/labs/37-openai-compatible-provider/solution test

Starter 应声明 实现 Provider 工具响应映射;Solution 应通过 6 项测试,全程无网络。

失败实现

ts
async function completeUnsafe(messages: unknown[]) {
  const response = await fetch(url, { method: "POST",
    headers: { authorization: `Bearer ${apiKey}` },
    body: JSON.stringify({ model, messages }) });
  if (!response.ok) throw new Error(await response.text());
  return await response.json();
}

失败实现把上游 JSON 直接泄给 Core,服务端 error body 可能含 key、prompt 或内部信息;没有 timeout、取消、状态分类和重试上限;fetch 是全局依赖,测试可能误访问网络。调用者还必须理解每个兼容服务的字段,隔离层名存实亡。

内部与外部 signal 组合需要真正 abort fetch:

ts
const controller = new AbortController();
const onAbort = () => controller.abort(parentSignal?.reason);
parentSignal?.addEventListener("abort", onAbort, { once: true });
const timer = setTimeout(() => controller.abort("provider timeout"), timeoutMs);
try {
  return await fetch(endpoint, { ...request, signal: controller.signal });
} finally {
  clearTimeout(timer);
  parentSignal?.removeEventListener("abort", onAbort);
}

关键实现讲解

内部 AbortController 同时响应外部 signal 与 timeout。catch 根据哪个信号触发区分“已取消”和“超时”。HTTP error 不回显响应正文;最终消息再次 replace API key,形成纵深脱敏。

构造函数验证 timeout 正整数、maxRetries 非负整数、model 非空、baseUrl 是允许 HTTP(S) URL、fetch 是函数。endpoint 使用 URL API 或规范去尾斜线追加固定路径,不能让模型提供 path。credential 空白在 complete 开头拒绝,Provider factory 还要绑定 key 与 endpoint host。

ModelRequest 的 system/user/assistant/tool 消息逐项映射,未知 role 或非字符串内容拒绝。工具 definition 转成上游 functions schema,name/description/inputSchema 已由 ToolRegistry 验证。不要把 Runtime 内部审批、workspace 或 secret metadata 全量序列化;请求只包含模型完成所必需内容。

请求 body 有字节上限,Trace 只记录模型、消息数、工具数、token 估算与 hash,不记录完整 prompt。Authorization header 永不进入调试对象。若 HTTP client 支持 request logging,默认关闭 header/body,或使用明确 redaction hook;错误对象也不能保存整个 Request。

一次 complete 创建独立 controller 与 timer,不能在 Provider 实例共享,否则一个调用取消会 abort 另一个。父 signal 预取消时 fetch 零调用;运行中 listener 只属于本次请求,finally 无论成功失败移除。timer 使用适当 unref 策略,Provider 请求仍由 Promise 生命周期持有。

timeout 必须 abort fetch,而不只是 Promise.race 返回。race loser 会继续占连接、消耗 Provider token并可能晚写 Trace;AbortSignal 让 HTTP 层停止读取。某些服务已经收到请求并完成计费,即使本地 timeout,也不能断言零成本,usage 标 unknown 并由账单对账。

外部取消和 timeout 同时发生时,通过父 signal 状态或独立 timeout 标志确定稳定错误:用户/Orchestrator取消为 PROVIDER_ABORTED,内部 deadline 为 PROVIDER_TIMEOUT。不要依据 DOMException message,不同运行时文本不同。完成路径只 settle 一次,finally 清所有资源。

重试前先按 HTTP status 分类。429 和 500、502、503、504 通常暂时可恢复;400 schema、401/403 auth/permission、404 endpoint 与其他 4xx 不重试。网络错误只有在 controller 未 abort 且 attempt 未耗尽时重试。解析/schema 错误一般不重试,因为相同响应协议不会因立刻请求改善。

maxRetries 表示首次请求之外的额外次数,循环 attempt <= maxRetries,总调用为一加上限。off-by-one 会让预算超出。每次 attempt 共用总 deadline还是独立 timeout要明确;课程共用一次 complete deadline,防重试无限延长。生产可有 per-attempt 与 overall 两层控制器。

退避使用指数基数、上限和 jitter,尊重合法 Retry-After 但不超过剩余 deadline。测试注入 sleep/clock,不真实等待。父 signal 在 sleep 中也能取消;sleep timer finally 清理。没有退避的紧密重试会在 Provider拥塞时放大压力。

重试是否安全取决于操作幂等性。普通 chat completion 通常不直接执行工具,重复请求最多产生不同候选和费用;若服务支持 server-side tool execution 或有幂等 key,则策略不同。客户端可发送 request id/idempotency key 并记录 attempt,不能把多个成功响应混合。

HTTP 非 ok 错误不直接输出 response body。可以解析受限 error code 供内部分类,但 message 经过 schema、字节与秘密清洗;用户看到 status、requestId 与行动建议。401 提示凭证,429 提示稍后/配额,5xx 提示服务暂时不可用。响应 header 也可能敏感,只白名单 request-id/retry-after。

成功 response 先检查 content-type、字节上限与 JSON 语法,再严格验证 choices 非空、首 choice 是对象、message 与 finish_reason 类型。任何 ?. 最终变 String(undefined) 的写法会把坏 schema伪装成工具名“undefined”;生产解析器用判别 schema 明确失败。

action 优先级是 tool_calls 高于 content。兼容服务可能同时返回解释文本和工具请求;Agent Loop 必须先执行工具,否则把“我将读取文件”当普通 message,实际工具永不运行。若多个 tool_calls,内部 contract 支持单个时要明确拒绝多项或按序排队,不能静默只取第一丢掉其余。

工具 call 验证 id/name 非空、name 来自请求提供的工具集合、function.arguments 是 JSON 字符串且解析为允许 input。arguments 解析失败返回 PROVIDER_INVALID_TOOL_ARGUMENTS,不让 Agent Core/tool schema看到任意原文。错误不回显完整 arguments,它可能含路径或秘密。

message content 必须非空字符串;空字符串不能形成一个无进展 action,否则 Loop 可能重复。没有 tool 和非空 content 时,根据 finish_reason 生成 finish;stop、length、content_filter 等映射内部完成/失败语义。length 通常是输出预算耗尽,不应一律当成功 stop。

usage 将 prompt_tokens/completion_tokens 归一 inputTokens/outputTokens,验证有限非负整数。缺 usage 可设零加 usageEstimated 或返回未知,不能用 NaN。cached/reasoning token 可扩展 breakdown,但 Agent budget 至少按总输入输出计;Provider pricing 在独立层版本化。

ts
it("maps tool before content and parses arguments", async () => {
  const value = await provider(fakeFetch(toolResponse({
    id: "c1", name: "read_file", arguments: '{"path":"a.ts"}',
  }))).complete([]);
  expect(value.action).toEqual({
    type: "tool", callId: "c1", name: "read_file", input: { path: "a.ts" },
  });
});

fetch 注入提供确定测试与安全保证。baseUrl 用 .invalid,测试 fetch 是 vi.fn,若代码误用 global fetch 会立刻失败或由网络禁用门禁发现。fixture Response 覆盖 message、tool、finish、429→success、never abort 与错误种子;测试不需要真实 key、费用或网络波动。

错误脱敏至少 replace 当前 apiKey、Bearer 模式和常见 secret key;不要只对最终 error.message 做一次,因为 Trace/debug 可能在中间捕获原异常。最稳妥是不读/传播远端 body,结构化错误只含白名单字段,再做最终 sanitize 作为纵深防御。

当 key 为空,fetch spy 为零;当错误对象主动包含 sk-seeded,终端/Trace/throw message 都不含它。用假种子测试,不用真实格式凭证。若 replaceAll 空字符串会在每字符间插入占位符,所以必须先完成 missing-key 预检再进入 catch sanitizer。

流式响应扩展需要增量 SSE decoder、每事件大小、UTF-8 chunk、工具参数分片和 backpressure。外部 cancel 关闭 reader/body;usage 常在最后一帧,提前取消时 unknown。先把非流式 contract 完整稳定,再实现 Stream Action,不能让 Core 直接消费厂商 SSE。

连接池、代理与 DNS 在 fetch 实现层配置,Provider 接口仍只关心请求/响应。代理环境可能泄漏 endpoint/credential,组织策略明确允许;自定义 CA 与 TLS 错误分类为 transport。不要在认证失败时自动切换未知备用 baseUrl,credential host 绑定必须保持。

速率限制在 Provider Manager 共享,不是每 complete 独立重试。token bucket 按 endpoint/model/tenant 控制并发与 QPS,429 更新冷却;排队请求仍响应 Abort。单实例 maxRetries 无法防十个 Agent 同时形成重试风暴。

多 Provider fallback 是显式路由:主服务故障后是否把 prompt 发到另一组织/地区涉及数据政策和成本,不能自动。fallback contract 记录目标、模型差异与用户批准;相同工具 schema 未必兼容。报告标出实际 Provider,Eval 不混淆模型版本。

缓存 completion 对 Coding Agent 风险较高:上下文、工具和仓库 hash 必须完全相同,且响应工具 call 可能不再适用于变化 workspace。默认不缓存有副作用规划;可缓存纯只读分类,key 包含完整规范请求 hash 与 Provider版本,敏感内容加密并短 TTL。

Provider Trace 为每 attempt 记录开始、status、requestId、耗时、usage、retry reason 与安全错误,不记录 header/body。一次 complete 的多个 attempt 用同 callId 关联,最终 action hash进入 Agent Trace。这样可解释成本与延迟,又不复制 prompt。

契约测试应在目标兼容服务升级前运行录制的无秘密 fixture,验证字段映射;真实 smoke 仅在受控 staging key 下显式触发,不进入普通 CI。录制 response 先脱敏、裁剪和许可证审查。线上 schema漂移触发 INVALID_RESPONSE,不用宽松 any 猜测继续。

完整验收 message 返回 hello 和 usage 三/二;tool call 解析 read_file 参数;无 content 映射 finish;空 key 零 fetch;429 后第二次成功且总次数二;never 在二十毫秒收到 signal abort;错误含种子 key 时对外完全消失。再验证 timer/listener 无泄漏。

system fingerprint 与 model revision 若服务返回,应作为可选 metadata 记录,帮助解释同 model 名行为漂移;不能让未知字段进入 Core 决策。请求中的 model 也要与响应 model 对照,服务若静默降级可告警。Eval 报告绑定实际 revision,避免把服务端换模误认为代码回归。

工具 choice 策略属于 ModelRequest:auto、none 或指定 tool。Provider 只映射受支持值,未知能力在发送前拒绝;不能为了兼容删除字段后继续,因为 Agent 可能期待必须调用测试工具。capability discovery 或静态服务配置告诉 Runtime 是否支持 tools、parallel calls、json schema 和 stream。

结构化输出若使用 response_format/json_schema,与工具 arguments 一样严格解析。服务返回包裹 Markdown 或修复后 JSON 时,不要静默正则提取并当有效;可以返回 INVALID_STRUCTURED_OUTPUT 让 Agent 重试一次带修正提示。schema 与响应字节受限,防深层对象资源攻击。

上下文窗口在发请求前估算。Provider 提供模型 maxContext,Context Builder 预算系统、消息、tools schema 与期望输出;超过则不发 fetch,返回 CONTEXT_LIMIT 并建议压缩。只依赖服务端 400 会浪费延迟,也可能把整个超大 prompt 发送到外部后才失败。

token 估算与服务实际 usage 有偏差,最终 ledger 用服务值;缺 usage 保留估算和 uncertainty。工具 schema 可能占大量输入 token,应纳入每步成本,不能只统计消息。重复重试每 attempt 都可能计费,总 usage/estimated cost 累计,而最终 action 只取成功 attempt。

预算检查在 request 前使用剩余 input/output/cost,预计超过则 BUDGET_EXCEEDED 零 fetch。流式过程中达到 output 上限要 abort;服务端仍可能多计少量缓冲,报告实际。Provider 不自行增加预算或换更贵模型,Orchestrator 决定重新规划。

内容过滤与安全拒绝要映射稳定 finish/failure。content_filter 不是普通 stop,Agent 不应声称完成;返回 blocked/provider_policy 并保留安全类别。服务拒绝正文不回显用户敏感 prompt。不同兼容厂商 reason 做 adapter 表,未知 reason 标 unknown。

429 可能代表速率、配额或并发,不一定都应重试。读取白名单 error code 与 Retry-After 区分:硬配额不足直接失败,短期 rate limit 退避。若没有可靠字段,有限重试后返回 RATE_LIMITED。错误 data 不进入用户输出,内部只保留安全 code。

HTTP 200 也可能包含 error 对象或 choices 空数组,必须按 schema 拒绝。反过来某些代理用非标准状态但合法 body,不应为“兼容”放宽默认;为该服务写显式 adapter。兼容层如果用无数 optional chaining 与默认零,会把协议故障变成错误 Agent行为。

重试后的响应可能不同,Trace 记录每 attempt 的 status/latency 与最终 chosen attempt,但不保存多个完整文本。若第一次请求服务端成功但连接断开,第二次可能产生另一个 tool action;没有幂等保障时标 uncertainty。对写动作,实际副作用仍只发生在 Agent 收到并批准 tool call 后,Provider 本身不执行工具。

证书、DNS、连接重置、代理认证和 HTTP timeout 分成 transport code。用户错误建议不同:DNS 检查 endpoint,TLS 检查 CA/时间,401 检查 key,429 等待/配额。原始系统 error 可能含 hostname/IP/路径,终端只显示批准 endpoint host 与安全摘要,详细诊断进受控 Trace。

隐私政策决定哪些消息可发到哪个 Provider。Request 每段带 data classification,router 在 complete 前验证 region、retention 和 training policy;不允许则 POLICY_DENIED 零 fetch。用户选择 OpenAI-compatible endpoint 不自动同意把所有仓库代码外发,尤其企业与个人信息。

日志保留与 Provider 服务端政策也要记录。若服务支持 no-store/zero retention header,配置显式发送并验证目标支持;不能把自定义 header 当保证。README 说明数据去向和用户责任。敏感 workspace 可选择本地模型 Provider,内部 ModelProvider contract 保持一致。

模型输出是不可信输入。message 可能含终端控制字符,tool arguments 可能路径穿越,finish summary 可能泄密;Provider 只做协议解析,后续 CLI sanitize、Tool schema、Workspace Boundary 与 Policy 继续防护。不要因输出来自付费模型就跳过任何本地安全层。

请求并发与取消要保持每调用隔离。Provider 实例可以共享 fetch/limiter,但 controller、timer、listener、attempt 和 requestId 都是局部。测试并发一个取消一个成功,确认取消不影响兄弟。全局熔断由 Manager 做,单次 catch 不修改其他请求状态。

熔断器统计指定 endpoint 的连续 transport/5xx,达到阈值在冷却期快速失败零 fetch;401 不应触发服务不可用熔断,而是 credential 状态。half-open 只放少量探测,成功恢复。熔断 status 进入 CLI行动建议和 Eval infrastructure failure,不算 Agent 能力失败。

Provider 初始化不应发送网络请求;真实 health check 是显式命令并使用最小无敏感 prompt,仍可能计费。普通 run 的首次 complete 即验证服务。这样 --help、config 和 dry-run 在断网/无 key 下仍可工作,符合 CLI 分层。

dry-run 可构造并验证请求大小、模型/tool capability、预算和 endpoint trust,但不显示 Authorization 或完整 body,也不 fetch。输出消息数、schema bytes、预计 token 与权限摘要。它帮助用户在发送代码前确认边界,不能声称验证 credential 或服务可用。

流式实现将 delta 组合成内部事件,工具 arguments 常跨多个 chunk,只有 finish 后才 JSON parse;中途断流不得执行半个工具。每个 choice/index/call id 维护有界 builder,多 tool 并行按 id 分开。完成后再产出 Action,或给 UI 安全文本进度但不驱动工具。

SSE parser 只接受 data: 事件、处理 [DONE]、限制单事件与总字节,忽略心跳空行。response.body cancel 与 controller abort 双向配合;reader finally releaseLock。网络断流后是否重试整个 stream 要谨慎,已展示文本但未执行工具可重新开始,Trace 标 attempt。

测试可用 ReadableStream fixture 分块切开中文与 JSON,验证 decoder、不完整工具拒绝、abort 关闭 reader和 usage 最后帧。非流式六测试仍作为共同 contract,流式只是另一路径。真实 SDK 若接入也要通过相同 action fixture。

模型安全回归集覆盖提示注入、恶意 tool name、超大 arguments、空 content、多个 choices、多 tool、负 usage 与 secret echo。Provider 层拒绝结构错误,工具/策略层拒绝行为错误。将故障放到正确层,错误 code 和评测归因才有意义。

发布新 adapter 时先跑共享 ModelProvider contract,再跑服务特有 fixture和受控 staging smoke;记录 endpoint API version、SDK/fetch version 与模型 revision。滚动发布可按小流量观察 INVALID_RESPONSE、429、timeout 和 token变化,超过阈值自动回滚。

运维看板按 Provider/model/status 聚合 QPS、P50/P95、input/output tokens、重试、timeout、rate limit、schema error 与 cost,不使用 prompt/runId 高基标签。突然 usage 翻倍可能是工具 schema 或服务 tokenizer 变化,和成功率一起评估。

一次事故复盘应回答:请求是否在预算/政策内发出,哪次 attempt、何种状态、是否真正 abort、服务是否可能计费、key 是否出现在任何证据、返回 schema 如何映射。Provider 作为隔离层的价值,就是让这些问题不需要阅读整个 Agent Loop 才能回答。

最终承诺是外部服务再不稳定,也只能通过一组有限 Action、usage 和稳定错误影响 Core;任何 credential、厂商 schema、重试细节或后台请求都不越界。达到这个标准,Fake 与真实 Provider 才能安全互换,端到端测试也能保持确定。

代码评审可以沿一张边界表检查:请求前校验哪些字段,哪些状态能重试,谁拥有 timer 与 signal,响应的每条 Action 由哪些 schema 证明,usage 缺失如何表达,错误允许携带哪些字段。任何依赖远端“通常会这样返回”的假设都要变成 fixture 或显式失败。

成本事故常来自重试与超时的交界。客户端超时后服务可能继续生成,随即重试又产生第二份费用;因此 timeout 要真正 abort、attempt 全部入账、未知 usage 不归零。对支持 idempotency 的服务发送稳定 request key,对不支持的服务用更保守 deadline 与重试策略。

安全事故常来自调试便利。临时打开 HTTP body logging、把 curl 命令复制到错误、在 Trace 保存完整 response 都会绕过正式脱敏。Provider 模块提供安全诊断摘要,团队 runbook 禁止用通用网络日志捕获生产凭证;需要深度排查时使用隔离测试 key 与无敏感 fixture。

课程验收之后可以替换 fetch fake 为本地 mock server 做一次协议集成,仍不访问公网。它验证真正 HTTP header、URL、abort socket 与字节限制;单元 fixture 验证分支,mock server 验证 transport,两层结合比直接用真实云服务更稳定且无费用。

上线前再用专门 staging 凭证做一条最小真实请求,确认 endpoint、模型能力和账单元数据;该 smoke 显式触发、失败不泄密,也不替代无网络契约套件。真实服务证明连通性,确定 fixture 证明所有关键分支,两类证据不可互换。

适配层的所有外部不确定性都必须被明确命名和约束。

运行与验证

bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 37
pnpm --filter @coding-agent/model test

证据覆盖 message+usage、tool+finish、missing key、429 后成功、20ms timeout 和包含种子 key 的错误脱敏。

真实运行输出

text
✓ tests/lab.test.ts (6 tests)
Test Files  1 passed (1)
Tests       6 passed (6)

项目 model 套件还要验证 Fake 与兼容 Provider 共享 ModelProvider contract。测试全程 fetch 注入且 baseUrl 为 .invalid,网络访问次数由 spy 精确证明。

常见失败与排查

故障案例 1

症状:response 同时含 content 与 tool_calls 时 Agent 只显示说明文字,工具从未执行。

根因:映射先判断 content,或只支持旧 function_call 字段而无契约测试。

定位:构造同时字段 fixture,检查内部 action 与 ToolRegistry 调用;记录 schema version。

修复:明确 tool 优先、严格解析 id/name/arguments;多工具按内部 contract 拒绝或完整排队。

故障案例 2

症状:401/400 反复请求并重复计费,429 时无退避形成风暴;maxRetries 一却调用三次。

根因:所有非 ok 都重试,attempt 上限 off-by-one,缺共享限流。

定位:逐状态码注入 response,断言 fetch 次数、sleep、总 deadline 和 error code。

修复:只对 transport、429 与指定 5xx 有界重试,次数一加 maxRetries;指数退避并响应取消。

故障案例 3

症状:CLI 已报告超时/取消,后台 fetch 仍运行;错误或 Trace 出现 API key/服务端 body。

根因:只 Promise.race、未传 signal,catch 原样传播远端 response/error。

定位:never fetch 监听 abort,种子 key 注入 Error/body,扫描所有输出和活动请求。

修复:每调用独立 AbortController 合并父 signal/deadline;白名单结构化错误,禁止 body 回显并最终脱敏。

课后作业

加入流式响应、Retry-After、指数退避和多 tool call;使用契约 fixture 覆盖不同兼容服务的字段差异。

验收 Rubric

维度通过标准常见扣分
映射message/tool/finish 与 usage 正确上游结构泄漏核心层
预检无 key 不调用 fetch发出无效请求
韧性可重试状态有界重试全部重试或不重试
安全timeout abort、错误脱敏后台请求或 key 泄漏

总项目增量

总项目包:@coding-agent/model

总项目路径:packages/model/src/openai-compatible-provider.ts

总项目验证命令:pnpm --filter @coding-agent/model test

Provider 补齐真实模型入口;下一课在独立 Bug Fix 夹具上贯穿全部核心动作。

延伸阅读

  • OpenAI-compatible API surface。
  • Idempotent retry 与 Retry-After。
  • AbortController 和 streaming backpressure。

方案对比与工程取舍

直接使用厂商 SDK 能快速支持流式和高级字段,却可能把 SDK 类型、重试和日志策略渗进 Core;薄 HTTP 适配器代码更多但 contract 清晰、fetch 可注入。课程选择薄层以看清边界,生产可在内部用 SDK,只要仍封装成同一 ModelProvider 并关闭危险日志。

宽松兼容解析能接更多服务,却会把坏字段猜成有效 Action;严格按目标服务 fixture 验证会早失败但更可审计。Coding Agent 的工具调用有副作用,宁可 INVALID_RESPONSE 停止,也不要把模糊响应当 write 工具。兼容差异通过显式 adapter/version解决。

统一总 timeout 简单并限制最坏延迟,重试可用时间少;独立 attempt timeout 加 overall deadline 更灵活。首版使用一个 deadline容易证明,规模后加入分层预算。无论选择,外部取消优先且底层 HTTP 真正 abort。

下一课衔接

真实 Provider 已能产生与 Fake Provider 相同的内部 Action。下一课回到确定性 Fake 脚本完成端到端 Bug Fix,贯穿 search、read、edit、test、diff、finish,并在临时副本和源树哈希上证明“完成”不是模型自报,而是组合系统的可验证事实。

从零实现 Mini Code Agent Runtime