Skip to content

feat(div-anthropic): LLM 接缝 + 错误层脱离 @anthropic-ai/sdk (Phase 1-6) - #66

Merged
dahai80 merged 11 commits into
mainfrom
feat/div-anthropic
Aug 16, 2026
Merged

feat(div-anthropic): LLM 接缝 + 错误层脱离 @anthropic-ai/sdk (Phase 1-6)#66
dahai80 merged 11 commits into
mainfrom
feat/div-anthropic

Conversation

@dahai80

@dahai80 dahai80 commented Aug 15, 2026

Copy link
Copy Markdown
Owner

概述

去除 fusion-code 对 @anthropic-ai/sdk 的运行时耦合 — Phase 1-5 完成(错误层解耦),Phase 6 文档。完整方案见 ~/fusion/architecture/fusion-code-div-anthropic.md

本 PR 引入 provider 中立的 LLM 接缝 (seam),并用鸭子类型桥替代所有 instanceof SDK 错误类判定。编译后二进制 0 处 @anthropic-ai/sdk 错误类引用

⚠️ 不合并 — 用户要求"全面测试后再合并主干"。当前仅 MLX 单机 smoke 通过,云 provider 路径未测。本 PR 开着待全面验证。

变更

Phase 1-4: 接缝基础设施 + 主循环接入

  • src/services/llm/types.ts — provider 中立类型 (LlmAdapter/StreamChunk/GenerateOptions/LlmFailure)
  • src/services/llm/{sseStream,errors,httpClient}.ts — SSE 解析 / 错误分类 / 裸 HTTP 客户端
  • src/services/llm/{adapter,mlxAdapter,registry}.ts — AnthropicWireAdapter + MLX 适配器 + 注册表
  • src/services/llm/seam.tsisSeamActive() + streamViaSeam(): flag LLM_ADAPTER_SEAM 开 + provider∈{firstParty,fusionMlx} 时,POST /v1/messages 直连,SSE→StreamChunk→SDK parts 喂下方既有 switch(零 switch 改动)。回滚=关 flag。

Phase 5: 错误层脱离 SDK

鸭子类型桥 src/services/llm/errors.ts (isApiErrorLike/isConnectionErrorLike/isTimeoutErrorLike/isAbortError),按形状判定 (status/headers/requestID/name/message),同时接受 SDK APIError(flag-off)与接缝 LlmRequestError(flag-on)。修复了接缝错误缺 .status/.headers 落穿所有 instanceof APIError 分支的接缝正确性 bug。

12 个文件脱 SDK 运行时:

  • withRetry.ts / api/errors.tsinstanceof APIErrorisApiErrorLike, APIConnectionErrorisConnectionErrorLike, throw APIUserAbortErrorabortError()
  • utils/errors.tsisAbortError.name/.message(minified build SDK 类名混淆为 nJT)
  • 7 处 APIUserAbortError: bashPermissions / permissions / useCanUseTool / GenerateStep / awaySummary / compact / claude → isAbortError()
  • 5 处 APIError: logging / claudeAiLimits / validateModel (NotFoundError→404, Auth→401) / claude / rateLimitMocking (本地 MockAPIError extends Error)

Phase 6: 文档 + 测试修复

  • docs/model-providers.md / README.md — 接缝与错误层文档
  • tests/cli/startup.test.ts — 修复 provider 检测用例环境变量泄漏 (FUSION_BASE_URL 未清理)

验证

  • typecheck: 0 errors
  • 单元测试: 113 pass / 0 fail
  • 集成测试: 320 pass / 0 fail
  • build (seam-enabled --dev --feature=LLM_ADAPTER_SEAM): green
  • 编译后二进制 @anthropic-ai/sdk 引用: 0
  • MLX smoke (真实模型 mlx-community--Meta-Llama-3.1-8B-Instruct-4bit): 200 OK,全链路 httpClient→MLX→SSE→StreamChunk→渲染

遗留(已开 issue,不阻塞本 PR)

接缝当前只覆盖 firstParty+fusionMlx;云 provider 仍走 SDK new Anthropic(...),故 @anthropic-ai/sdk 仍是运行时必需依赖(package.json 未动)。

回滚

关闭 LLM_ADAPTER_SEAM flag(默认即关)→ streamViaSeam 经 Bun DCE 消除,回退 SDK 路径。错误层鸭子桥对 flag-off 的 SDK 错误同样生效,无功能损失。

🤖 Generated with Claude Code

dahai80 and others added 11 commits August 15, 2026 11:03
基线: typecheck/test/build 全绿, 38 tests 0 fail, bundle 162.6MB
审计: 运行时 SDK import 6 文件, 类型垫片 103 消费者, 5 个 @anthropic-ai/* 依赖

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新增 src/services/llm/types.ts: provider 中立 LLM 接缝类型, 参考 deepseek-harness
- StreamChunk 联合与 claude.ts 现有 SSE switch 分支逐一映射
- LlmFailure 稳定错误码 (AUTH/RATE_LIMIT/...) 替代 instanceof APIError
- LlmAdapter 接口: 唯一必需方法 stream() (静态分派, 非 Cordis)
单测: 适配器契约/chunk 穷尽性/错误码稳定性 (4 用例, 38→42)

零行为变更: 纯新增类型层, 运行时仍用 Anthropic SDK
checkpoint: typecheck/test/build 全绿

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
去除 Anthropic SDK 的接缝层基础设施 (暂不接入主循环, 纯新增):

- src/services/llm/sseStream.ts: parseSseStream — 按 SSE 帧边界 (空行派发,
  data:/event:/: 注释) 解析 fetch Response.body, 多行 data 用 \n 连接,
  跨 chunk 边界 buffer carryover, AbortSignal 中断抛 AbortError。
  替代 SDK 的 Stream/BetaMessageStream (claude.ts 已绕过 BetaMessageStream)。
- src/services/llm/errors.ts: classifyError/isRetryable/LlmRequestError —
  provider 中立错误码 (AUTH/RATE_LIMIT/INVALID_REQUEST/SERVER/TIMEOUT/
  TRANSPORT/ABORTED), 替代 instanceof APIError。AbortError 优先且兼容
  DOMException 与任意 .name="AbortError" 的 Error。
- src/services/llm/httpClient.ts: postMessages — 复用现有 client.ts 的
  getUserAgent/getProxyFetchOptions/getAnthropicApiKey/computeCch/
  CLIENT_REQUEST_ID_HEADER, firstParty 走 cch 签名, 非 2xx/fetch 失败
  抛 LlmRequestError (含分类后的 LlmFailure)。
- 单测: sseStream 10 例 (含跨 chunk/心跳/中断), errors 20 例 (status/
  message/Abort 优先/重试判定)。types 4 例 (Phase 1) 共 34 例全绿。

checkpoint: typecheck ✓ / 72 tests pass ✓ / build ✓

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ag 守护)

接缝层适配器实现 (纯新增, LLM_ADAPTER_SEAM feature 守护, 默认关闭走 SDK):

- src/services/llm/adapter.ts: AnthropicWireAdapter
  - buildRequestBody: GenerateOptions -> /v1/messages JSON (model/messages/
    system[字符串|块数组]/tools->input_schema/max_tokens/stream/thinking/
    temperature/stop_sequences + extraBody 透传 betas/metadata 等扩展字段)
    依据 claude.ts:1539 paramsFromContext 实际形状。
  - sseToChunk: Anthropic SSE 事件 -> StreamChunk 中立词表
    (message_start->message-start, content_block_start->block-start,
    text_delta/thinking_delta/signature_delta/input_json_delta/
    connector_text_delta -> 对应 delta chunk, content_block_stop->block-end,
    message_delta->usage(记 stop_reason), message_stop->finish)。
    依据 claude.ts:1975 switch。signature_delta 归 thinking 块 (零宽+signature)。
  - AnthropicWireAdapter.stream: postMessages + parseSseStream + sseToChunk。
- src/services/llm/mlxAdapter.ts: createMlxAdapter
  - 复用 createFusionMlxFetch (fetch override, 内部 Anthropic<->OpenAI 转译,
    响应已 encodeStreamToAnthropicSSE), 故 MLX 路径直接复用 AnthropicWireAdapter
    的 SSE 解析。baseUrl 占位 (override 按 url.includes("/v1/messages") 拦截)。
- src/services/llm/registry.ts: getLlmAdapter(provider, model)
  - 按 APIProvider 静态分发。seam 关闭 (feature 宏 false) 返回 null -> 调用方
    回退 SDK, 实现 instant rollback。seam 期 firstParty+fusionMlx 走新适配器,
    bedrock/vertex/foundry/openai 暂返回 null (Phase 4/5 迁移)。
  - feature() 直接用在 if 里 (Bun DCE 宏约束, 不可包在返回它的函数中)。
- src/services/llm/httpClient.ts: PostMessagesOptions 增 fetchFn 可选注入
  (MLX 路径用 createFusionMlxFetch 作为 fetch)。
- scripts/build.ts: fullExperimentalFeatures 注册 LLM_ADAPTER_SEAM。
- 单测: adapter 29 例 (buildRequestBody 10 + sseToChunk 19), registry 4 例
  (seam 关闭可空契约; on-path 由带 flag 构建的集成测试覆盖)。共 63 llm 例全绿。

checkpoint: typecheck ✓ / 101 tests pass ✓ / build ✓ (含 --feature=LLM_ADAPTER_SEAM 构建 ✓)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
claude.ts 在 SDK messages.create 前注入 LLM_ADAPTER_SEAM 接缝分支:
flag 开 + provider∈{firstParty,fusionMlx} 时, 调 streamViaSeam 直接
POST /v1/messages 并把 SSE→StreamChunk→SdkPart, 喂给下方既有 switch,
零 switch 改动。flag 关走 SDK 原路径, 回滚=关 flag。

- seam.ts: isSeamActive (ternary, DCE 兼容) + streamViaSeam 核心流式
- chunkToPart.ts: StreamChunk→SdkPart 翻译器 (sseToChunk 的逆映射),
  usage chunk 携带 stopReason 于 message_delta 时 emit
- types.ts: usage chunk 扩展 stopReason 字段
- adapter.ts: sseToChunk message_delta emit stopReason
- claude.ts: import seam + 接缝分支 (queryCheckpoint + return)
- 测试: chunkToPart 12 项, adapter message_delta stopReason 断言更新

验证: typecheck ✓ / 113 tests ✓ / build(默认+flag) ✓ /
本地 MLX 真实模型 smoke (Llama-3.1-8B, seam 激活, status 200, 正确流式应答) ✓

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
withRetry.ts 不再 import @anthropic-ai/sdk 运行时类:
- instanceof APIError → isApiErrorLike (接纳 SDK APIError 与 seam LlmRequestError)
- instanceof APIConnectionError → isConnectionErrorLike
- new APIUserAbortError() → abortError() (name="AbortError")
- APIError 形参 → ApiErrorLike 形态类型

前置 (step 1):
- LlmFailure/LlmRequestError 暴露 SDK 兼容 .status/.headers/.requestID
- classifyError 接收 headers; httpClient 捕获响应头到 LlmRequestError
- 新增 isApiErrorLike/isConnectionErrorLike/isTimeoutErrorLike/isAbortErrorLike

修复 seam 路径正确性缺口: 之前 seam 抛的 LlmRequestError 无 .status/.headers,
会穿透 withRetry 所有 instanceof APIError 分支 → seam 错误不重试/不分类。
现形态桥同时覆盖 SDK 路径 (flag 关) 与 seam 路径 (flag 开)。

验证: typecheck 0 错; bun test src/__tests__/llm 75 pass;
tests/services 146 pass; build:dev --feature=LLM_ADAPTER_SEAM 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
errors.ts 不再 runtime import @anthropic-ai/sdk:
- instanceof APIError (×22) → isApiErrorLike
- instanceof APIConnectionError/TimeoutError → isConnectionErrorLike/isTimeoutErrorLike
- APIError 保留为 type-only (anthropic-protocol.ts, 构建期擦除), 用于
  categorizeRetryableAPIError 形参与 formatAPIError 调用点 cast

isConnectionErrorLike/isTimeoutErrorLike 升级为类型守卫 (error is Error),
使 errors.ts 在守卫后可读 error.message (原 instanceof 自动收窄的等价能力)。

验证: typecheck 0 错; bun test 259 pass (含 llm 75 + services 146);
build:dev --feature=LLM_ADAPTER_SEAM 绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
utils/errors.ts: isAbortError 改用 .name/.message 鸭子判断, 不再 instanceof SDK
bashPermissions / permissions / useCanUseTool / GenerateStep / awaySummary:
  instanceof APIUserAbortError → isAbortError()
compact.ts: abortError 工厂改为 () => Error{name:'AbortError'}, 不再 new APIUserAbortError

checkpoint: typecheck 0 / 113 tests pass / build green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
logging.ts / claudeAiLimits.ts / validateModel.ts / claude.ts / rateLimitMocking.ts:
  instanceof APIError → isApiErrorLike (按形状: status/headers/requestID)
  instanceof NotFoundError → isApiErrorLike && status===404
  instanceof AuthenticationError → status===401
  instanceof APIConnectionError → isConnectionErrorLike
  new APIUserAbortError() / new APIConnectionTimeoutError() → 具名 Error 工厂
  rateLimitMocking: 本地 MockAPIError extends Error 替代 SDK APIError 类 (ant-only)

checkpoint: typecheck 0 / 113 tests pass / build green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
beforeEach 未清理 FUSION_BASE_URL/FUSION_GATEWAY_ENABLED/FUSION_API_KEY,
shell 中 FUSION_BASE_URL=http://127.0.0.1 使 shouldAutoUseFusionMlx() 误判为 true,
导致 "有 FUSION_API_KEY 时不应自动启用" 用例失败。补齐清理使用例 hermetic。

(报错用例定位修复 — 与 div-anthropic 无关, 但按规则一并修复)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
model-providers.md: 新增 "LLM Adapter 接缝" 节 — 架构/错误层鸭子桥/范围与遗留(#63-65)
README.md: Build 节新增 LLM_ADAPTER_SEAM flag 说明 + 链接 docs
CLAUDE.md: 本地 (gitignored) 新增 seam 子系统指引

关联 issue: #63 (client.ts 云 provider) #64 (package.json) #65 (sdk/runtime/mcpb 评估)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant