Skip to content

feat: AIBridge Rust 重构(agn-sdk v1 -> aibridge v2.0.0)#2

Merged
WingkySky merged 55 commits into
mainfrom
feat/aibridge-rust-rewrite
Jul 13, 2026
Merged

feat: AIBridge Rust 重构(agn-sdk v1 -> aibridge v2.0.0)#2
WingkySky merged 55 commits into
mainfrom
feat/aibridge-rust-rewrite

Conversation

@WingkySky

Copy link
Copy Markdown
Owner

概述

将 Python agn-sdk (v1.3.3, ~19700 行) 用 Rust 重构为跨语言 SDK aibridge v2.0.0,支持五种语言直接 import。

主要变更

  • Rust 核心(aibridge-core):33+ provider,1448 单测
  • C ABI(aibridge-ffi):供 Go/JVM/.NET 调用
  • 五语言绑定:Python(PyO3)/ Node(napi-rs)/ Go(CGO)/ JVM(JNA)/ .NET(P/Invoke)
  • 全能力:chat/image/video/TTS/ASR/embed,含流式
  • CI/CD:跨平台矩阵,五语言打包脚本
  • 迁移指南:docs/migration-guide.md(Python v1->v2)

验证

  • 真实 API 跑通:agnes 全能力(chat/image/video)+ gemini chat
  • 1448 单测 + 39 FFI 单测全通过
  • CI 全平台通过

⚠️ 破坏性变更

v2 不兼容 v1(包名 agn-sdk -> aibridge,API 重新设计)。老用户请参考 迁移指南

文档

合并后

  • 打 v2.0.0 tag
  • 旧版 v1 归档(保留 v1 分支)
  • 发布到 PyPI(需单独操作)

WingkySky added 30 commits July 7, 2026 10:20
将 agn-sdk 用 Rust 重构为跨语言 SDK aibridge 的设计文档。
- 五语言(Python/JS/Go/JVM/.NET)原生 import
- Rust 核心 + 双入口绑定(PyO3/napi 直连 + C ABI)
- v2 破坏性升级,MVP 优先 agnes/火山/gemini/openai
- workspace 根 + 4 crate (aibridge-core/ffi/python/node)
- aibridge-core 与 aibridge-ffi 编译通过
- rust-toolchain + .gitignore 更新
- 实现计划文档
实现 .NET (C#) 绑定,通过 P/Invoke 调 aibridge-ffi cdylib。

实现文件:
- AIBridge.csproj:net8.0 类库 + 可执行 hello(OutputType=Exe, StartupObject=Hello)
- Native.cs:P/Invoke 声明全部 aibridge_* 函数 + SafeHandle 封装(字符串/字节自动释放)+ 运行时动态库解析(支持 AIBRIDGE_LIB_PATH 环境变量与仓库 target/debug 回退)
- Models.cs:数据模型(ChatRequest/ChatMessage/ChatCompletion/ChatCompletionChunk/SpeechRequest/SpeechResult 等),System.Text.Json 反序列化,字段名对齐 Rust serde snake_case
- AibridgeException.cs:异常体系(AibridgeException + 11 个子类,按 last_error 的 code 字段映射)
- Client.cs:Client 封装(IDisposable),Chat/Speech/ChatStreamAsync(IAsyncEnumerable<ChatCompletionChunk>),async 方法用 Task.Run 包装阻塞 FFI
- Hello.cs:hello world 示例(Program Main),验证 echo 适配器 chat 回显 + stream 3 chunk + speech 15 字节
- build.sh:构建脚本(cargo build ffi + dotnet build + dotnet run)

FFI 遗留问题处理:
1. last_error 线程局部:每个 FFI 失败后同线程立即读 last_error 转存(ChatStreamAsync 在 Task.Run 同一 ThreadPool 线程内读取)
2. stream_next 串行:await foreach 天然串行,不并发 next
3. aibridge_bytes_free / aibridge_string_free:用 AibridgeBytesHandle / AibridgeStringHandle(SafeHandle)兜底释放,MarshalAndFree 先拷贝后释放
4. client / stream 句柄:Client.Dispose(Interlocked 原子化防 double-free)调 aibridge_client_destroy;ChatStreamAsync finally 调 aibridge_stream_destroy

验收:
- cargo build -p aibridge-ffi 已产 libaibridge.dylib(target/debug/)
- dotnet SDK 未装(沙箱无法 sudo 安装),hello world 待 dotnet 环境验证
- build.sh 提供完整构建+运行流程,装好 dotnet 后执行 ./build.sh run 即可
实现 OpenAiCompatAdapter 作为 OpenAI 兼容协议(agnes/openai/azure 等约 80% 适配器)的共享地基,
子适配器(阶段1.1 openai / 1.2 agnes)可复用其 chat/chat_stream/image_generate/
embed/list_models 实现,仅 override base_url、provider_type、特有参数等差异。

主要内容:
- OpenAiCompatAdapter struct:持有 HttpClient、ProviderConfig、provider_type、
  provider_name、base_url、capabilities
- 核心方法实现 OpenAI 兼容协议:
  - chat:POST /chat/completions,构造请求体(model/messages/temperature/
    max_tokens/tools/stream 等),解析响应 → ChatCompletion
  - chat_stream:POST /chat/completions stream=true,自实现 SSE 行流解析器
    (LinesStream)解析 data: <json> → ChatStream,支持心跳/空行/[DONE]
  - image_generate:POST /images/generations → ImageResult
  - embed:POST /embeddings → EmbeddingResult
  - list_models:GET /models → Vec<ModelInfo>(含模型类型推断)
- 参数映射:预置 openai_compatible_mapping()(用 ParameterMapping 机制,OpenAI
  协议即通用名故透传)
- 错误映射:map_api_error 提取 OpenAI error.message / retry_after,
  按 HTTP 状态码分类(401→Authentication,429→RateLimit+retry_after,
  404→ModelNotFound,400→Validation,5xx→Api)
- 请求体构造:通用参数序列化 + extra 透传 + 流式附带 stream_options.include_usage
- 设计为可复用:方法均为 pub,子适配器通过组合委托调用,Rust 无继承故采用
  '地基结构 + trait 方法转发' 模式

单测(45 个,mockito mock HTTP):
- chat 正常路径:解析 completion / 发送 temperature+max_tokens / extra 透传 /
  tool_calls 解析
- chat 错误路径:401/429(含 retry_after)/404/500/400/unsupported
- chat_stream:SSE 多 chunk 解析 / 心跳空行 / stream=true + stream_options /
  401 / 429
- image_generate:url 解析 / b64 解析 / 401 / 429 / unsupported
- embed:多向量解析 / 单输入 / 401 / 404 / unsupported
- list_models:正常 / 类型过滤 / 401 / 429 / 500
- 错误映射单元测试 + 参数映射测试 + base_url 兜底 + build_chat_body 测试

新增 dev-dependencies:mockito 1
adapters/mod.rs 导出 openai_compat 模块;暂不注册到工厂(阶段1.1/1.2 注册)。

验收:cargo test -p aibridge-core 235 通过(190 原有 + 45 新增),
clippy --all-targets -D warnings 无警告,cargo build -p aibridge-ffi 通过,
cargo fmt 已格式化。
五语言错误 code 对齐 Rust aibridge-core error.rs 的 AibridgeError::code()
实际返回值(authentication_error / rate_limit_error / timeout_error 等):
- Go (bindings/go/error.go):errCodeAuthentication/RateLimit/Network/Timeout 四个
  常量补齐 _error 后缀,更新注释示例
- .NET (bindings/dotnet/AIBridge/AibridgeException.cs):MapByCode switch 与
  Authentication/RateLimit/Timeout 三个子类构造的 code 字符串统一为 Rust 实际值
- Python/Node/JVM 原本已对齐,无需修改

跨语言一致性验证(echo adapter):
- 四语言 hello world(Python/Node/Go/JVM)chat/stream/speech 输出语义一致
  (chat="hello [echo]",stream 3 chunk 拼接="hello [echo]",speech=15 字节)
- 四语言未知 provider 错误探针 code 均为 provider_not_found
- .NET 因本机无 dotnet SDK 仅做代码级对齐确认
- 一致性测试文档与探针脚本置于 tests/consistency/
aibridge-ffi 和 aibridge-python 的 [lib] name 都为 "aibridge",导致
cargo build 把两个 cdylib 都输出到 target/debug/libaibridge.dylib 互相覆盖。
maturin develop 后 dylib 变成 PyO3 版本(无 C FFI 符号),Go/JVM 链接失败。

修复(仅改 aibridge-python,ffi 的 [lib] name="aibridge" 保持不变以匹配
Go/JVM/.NET 的 -laibridge 链接):
- Cargo.toml: [lib] name 从 "aibridge" 改为 "_aibridge",cdylib 产物变为
  lib_aibridge.dylib,与 libaibridge.dylib 分离
- src/lib.rs: #[pymodule] 函数名从 aibridge 改为 _aibridge(生成 PyInit__aibridge
  符号与 lib name 一致),加 #[pyo3(name = "aibridge")] 保持模块 Python 名
- pyproject.toml: [tool.maturin] 加 module-name = "aibridge",把 Rust pymodule
  _aibridge 映射到 Python 顶层模块 aibridge,import aibridge 不受影响

验证:
- maturin develop 无 warning,import aibridge 正常
- examples/hello_python.py chat/stream/speech 全部通过
- cargo build -p aibridge-ffi 产 libaibridge.dylib(6 个 C FFI 符号,0 PyInit)
- Go hello world 链接 libaibridge 通过
- JVM hello world 通过
- cargo build --workspace 0 warning
Python(crates/aibridge-python/src/lib.rs):
chat_stream 的 __anext__ 原先用 RUNTIME.block_on 预取 chunk,echo adapter
无 IO 瞬时完成能跑通,但真实 adapter(openai/agnes/gemini 的 reqwest IO)
会阻塞 asyncio 事件循环。

重构为 PyO3 0.28 内置 Coroutine 桥接模式:
- __anext__ 同步返回 pyo3::coroutine::Coroutine(可 await 的 Python 对象),
  其包裹的 Rust future 在全局 tokio runtime 上 spawn 消费 core ChatStream。
- 真实 IO 在 tokio worker 线程执行,asyncio 线程仅 await JoinHandle(Pending
  时让出,其他协程可运行),不阻塞事件循环。
- Coroutine 的 AsyncioWaker 自动通过 asyncio.Future + call_soon_threadsafe
  把 "chunk 就绪" 通知桥接回 asyncio 事件循环(PyO3 内置实现,无需手写
  asyncio loop 引用),await 期间让出线程。
- 移除手写的 NextAwaitable(__await__/__iter__/__next__ 协议)及模块注册,
  改用 Coroutine 内置的 send/__next__/__await__。
- GIL:future 在 tokio 上 await stream 期间不持 GIL,仅在拿到 chunk 后
  Python::attach 构造 pyclass。
- 流结束 future 返回 Err(StopAsyncIteration);chunk 出错返回对应
  AibridgeError 子类;正常 chunk 返回 Ok(chunk) → StopIteration(chunk)。

Node(crates/aibridge-node/src/lib.rs):
经核查无需重构。当前 mpsc channel + #[napi] async fn next() 模式不阻塞
libuv 事件循环:napi 2.x 用多线程 tokio runtime,#[napi] async fn 的
future 与 spawn 的消费 task 都在 tokio worker 线程调度,recv().await
Pending 时让出 tokio 线程,libuv 事件循环自由运行。JS await Promise 让
其他任务运行,与 Python 端机制等价。

验证:
- cargo build --workspace 通过
- cargo clippy -p aibridge-python -p aibridge-node -- -D warnings 无警告
- maturin develop + python examples/hello_python.py:echo chat_stream
  3 chunk 拼接 'hello [echo]' 正确
- napi build + node examples/hello_node.js:echo chatStream 3 chunk
  拼接 'hello [echo]' 正确
- 真实 IO(openai/agnes/gemini)未验证(无 API key),但重构基于
  "tokio task 消费 stream + asyncio await Future" 的非阻塞推理
WingkySky added 25 commits July 7, 2026 21:44
新增 docs/PROGRESS.md:完整进度(阶段0-2a 完成,23 provider,810 单测)+ 接手指南(环境、命令、worktree 模式、阶段 2b/2c 要点、关键约束)+ 提交历史,供其他 agent 接手。
AGENTS.md 顶部加 Rust 重构指向提示。
实现 KlingAdapter(独立协议,非 OpenAI 兼容):
- video_create:POST /videos/generations(文生视频)/ /videos/image2video(图生视频)
- video_poll:GET /videos/generations/{task_id}
- list_models:硬编码 kling-v1 / v1-5 / v2
- 认证:Bearer Token;默认 base_url https://api.klingai.com/v1
- 状态映射:submitted/queued→pending、processing、succeed/success→success、failed/error→failed
- 视频 URL:data.task_result.videos[0].url;错误:task_status_msg/error
- 错误映射:401→Authentication、429→RateLimit、404→ModelNotFound、400→Validation、5xx→Api
- 不支持能力(chat/image/embed/audio)走 trait 默认实现返 UnsupportedCapability
- 71 个 mockito 单测覆盖正常/错误/边界路径

对应 Python v1 agn/adapters/kling.py。
ElevenLabs TTS 适配器(独立协议,xi-api-key 认证)。

- speech: POST /text-to-speech/{voice_id},二进制音频返回
- list_voices: GET /voices,按语言过滤;recommend_voices 复用默认实现按性别+limit 过滤
- 错误映射: 401→Authentication / 429→RateLimit / 400→Validation / 404→VoiceNotAvailable / 5xx→Api
- 内置 46 个音色别名表(Rachel→voice_id),未知名称原样透传
- 64 个单测,覆盖正常/错误路径(401/429/400/404/500)/不支持能力/纯函数
- .github/workflows/ci.yml: 6 job 跨平台矩阵(Rust core+ffi / Python /
  Node / Go / JVM / .NET),覆盖 linux/macos/windows × amd64/arm64,
  缓存 cargo/npm/gradle,触发于 push 到 feat/aibridge-rust-rewrite + PR
- scripts/: 7 个 bash 打包脚本(build-ffi/python/node/go/jvm/dotnet
  + release 一键构建),中文注释,set -euo pipefail,可本地运行
- package.json: napi.triples 配置多平台目标(defaults + additional)
- build.gradle.kts: copyNativeLib task 把 libaibridge 打进 jar
  (JNA classpath 约定路径 {os}/{arch}/,按平台 classifier)
- AIBridge.csproj: NuGet 打包 runtimes/{rid}/native/(.NET 标准)

不实际发布(无 token),仅 CI 配置 + 打包脚本就绪。
…els/list_voices)

在已有 chat/chat_stream/speech 基础上补全 Client 全部能力绑定:

新增方法(Client):
- image_generate(model, prompt, size, n, negative_prompt, reference_images,
  mask, response_format, **kwargs) -> ImageResult
- video_create(model, prompt, width, height, num_frames, frame_rate, mode,
  reference_images, negative_prompt, seed, **kwargs) -> VideoTask
- video_poll(task_id, model="") -> VideoStatus
- transcribe(model, file, language, prompt, response_format, temperature,
  **kwargs) -> TranscriptionResult
- embed(model, input, **kwargs) -> EmbeddingResult
- list_models(model_type=None) -> list[ModelInfo]
- list_voices(language=None) -> list[VoiceInfo]
- recommend_voices(language=None, gender=None, limit=10) -> list[VoiceInfo]

新增 11 个 #[pyclass] 数据模型:
- ImageData / ImageResult
- VideoTask / VideoStatus
- TranscriptionSegment / TranscriptionWord / TranscriptionResult
- EmbeddingItem / EmbeddingResult(含 get_embeddings())
- ModelInfo(type 字段经 #[getter(r#type)] 暴露为关键字 type)
- VoiceInfo

实现要点:
- **kwargs 经 Option<Py<PyDict>> 接收(async fn 需 'static future),
  经 py_to_json 转 serde_json::Value 填入 core extra 透传
- 字符串默认参数用 &str(async &self 方法支持借用,配合字面量默认值)
- file/reference_images/mask 支持 str(路径/URL)或 bytes,经 py_to_file_input
  转 core FileInput
- video_poll 的 model &str 先 to_string 再 spawn(满足 'static)
- TaskStatus 经 task_status_to_str 转字符串暴露给 Python
- 错误沿用现有 map_error 映射到 AibridgeError 子类层级

验证(echo adapter,免认证):
- cargo build -p aibridge-python 通过
- cargo clippy -p aibridge-python -- -D warnings 无警告
- maturin develop 成功
- examples/hello_python_full.py 全部能力通过(chat/image/video_create+poll
  /transcribe/embed/list_models/list_voices/recommend_voices/speech)
- 原 hello_python.py 回归通过
- .env.example:四 provider(agnes/openai/gemini/火山)key 模板,可上传 GitHub
- examples/test_real_providers.py:从 .env 读 key,list_models() 实时拉真实 model 名,测四 provider 各能力,不硬编码 key
- .env 已 gitignore,不上传 GitHub
…i_reference)

真实 API 测试发现 video_create 报 [validation_error] Input should be
'ti2vid', 'keyframes' or 'multi_reference'。原实现直接把统一 VideoMode
序列化为 text2video/image2video/keyframes/multiimage 透传,与 agnes 平台
要求的 mode 不一致。

新增 map_video_mode 将统一 VideoMode 显式映射到 agnes 平台 mode:
- Text2Video / Image2Video -> ti2vid
- Keyframes -> keyframes
- Multiimage -> multi_reference

build_video_body 改用映射函数;更新 5 处单测断言;新增 map_video_mode
单测覆盖全部 4 种 VideoMode 映射。
…nt 变量

- agnes 全能力(chat/image/video)真实 API 验证通过
- 火山 list_models 通,image/video 需接入点 ID(方舟规则)
- .env.example 加 VOLCENGINE_IMAGE/VIDEO_ENDPOINT 变量 + base_url 说明
- .NET:DllImportSearchPath.Default→null、SetHandle→构造函数、Assembly using、DllImportResolver 签名
- Go:CGO LDFLAGS 路径修正(target/debug→target/release)
- Python wheel:CI maturin build 路径修正(workspace target/wheels/)
- Go windows-x64:Rust 改用 x86_64-pc-windows-gnu target(与 MinGW gcc 兼容,修复 __chkstk 链接失败)
- Python macos-universal2:maturin --target universal2-apple-darwin(修复 --universal2 参数不存在)
- 移除已弃用的 macos-x64 job(Rust 核心+ffi / Node .node,24h 超时)
@WingkySky
WingkySky merged commit 65ac41f into main Jul 13, 2026
42 checks passed
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