- Rust 93.8%
- Python 6.2%
- protocol/glm.rs: 新增 glm-native 协议(官方 <tool_call>/<arg_key>/<arg_value>
格式),与 glm-xml/semixml 并列;masker 适配新标签
- protocol/infogather.rs: 锚工作流名可配(with_anchor_tool),教学/清单/
序列化全部用实例锚名;新增 custom_anchor_tool_renames_everywhere 测试
- config.rs/model.rs: ModelConfig 新增 anchor_tool 字段(空串回退 tool_call)
- prompt.rs + openai/{chat,responses}.rs: PromptMessage 新增 tool_name,
tool 结果消息按 name 回填(OpenAI 规范 name 可选,客户端常不填),
各 native flow 走 format_tool_result_named 输出调用回显
- dsml.rs/masker.rs/mod.rs: 配套调整与回归测试
实测:localhost8080/glm53-infogather(infogather + glm-native +
anchor_tool=tool_call_glm)端到端跑通子代理只读调研任务,挂起/恢复
链路正常。
|
||
|---|---|---|
| scripts | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| config.example.toml | ||
| DEPLOY.md | ||
| mock_hiagent.py | ||
| probe_kimi.py | ||
| README.md | ||
hiagent-proxy
把同济 HiAgent(会话式智能体 API)适配为 OpenAI 兼容接口的独立 Rust
代理:客户端只见到 /v1/chat/completions、/v1/responses 与 /v1/models,
上游始终是 HiAgent 的 create_conversation / chat_query_v2(SSE 流式)会话
协议。
连接方式逐条照搬 dsh-llm-hiagent (原生 DSH LLM 适配器)的设计,但本代理是无状态、可单独部署的 HTTP 服务, 架构参考 NIyueeE/ds-free-api。
特性
- OpenAI 双协议:
/v1/chat/completions(含reasoning_content、工具调用) 与/v1/responses(物品模型:message / reasoning / function_call,含流式事件) - 一个模型 = 一个 API Key:HiAgent 每个 agent 有独立 key,配置里按模型 id
绑定(
model_keys字典 +models[].api_key行内 + 顶层回退) - 六种工具协议:
deepseek-dsml(DeepSeek 原生 DSML 块)、json-toolcall(<tool_calls>+JSON 数组,与模型无关,实测最稳)、glm-xml、glm-semixml(GLM XML 变体)、kimi-xtml、infogather——解析、历史序列化、流式 masker、 截断恢复全部照搬 dsh-llm-hiagent(含服务端随机吞掉块开头的 relaxed 恢复)。 2026-09 实测结论:HiAgent 推理网关会原生解析并消费模型自家工具标记 (规范 DSML 在 agent 未注册平台工具时直接变空回复+兜底文案),因此 DeepSeek 模型推荐改用json-toolcall(端到端 8/8;DSML 依赖模型逐字模仿 U+2502 变体,偶发退化)。 infogather协议(information_gathering 工作流):把工具调用包装成对平台 工作流tool_call(参数名恒为input,值为{"tool":…,"args":…}JSON 字符串) 的一次调用,模型走平台原生格式发出,平台据此挂起(SSEinterrupted)等待补充 信息,同会话下一条chat_query即恢复。它委托一个内层协议做语法:models[].format(缺省deepseek-dsml)决定用哪种原生格式教学与解析, 于是protocol = "infogather"+format = "deepseek-dsml"构成一个整体协议。- 真流式:
Accept: text/event-stream解锁逐 token 增量;think_message事件按类型路由为 reasoning(chat 的reasoning_content/ responses 的reasoning物品);DSML/XML 工具区实时遮蔽,结束做权威解析 - 会话双模式:
no_reuse = true(默认):每次请求新建 HiAgent 会话,完整历史作为只读 上下文发入首条消息(anti-self-Q&A 标题),彻底绕开服务端 ~3 轮滑动窗口no_reuse = false:复用会话,映射持久化到hiagent-conversations.json
- 容错:会话过期(400)自动重建重试一次;「失败兜底文案」(空回复)自动
换新会话重试一次(可关);空闲超时(默认 180s);token 计数来自
message_cost事件 - 下游鉴权:
[server] api_keys(Bearer)与上游 key 完全分离;可选
复用模式如何适配 OpenAI 格式接口
复用机制原本为 DeepSeek Harness(DSH)的会话模型设计:DSH 提供 sessionId
并逐轮发送增量消息。OpenAI 接口是无状态的,代理做了两处适配:
- chat completions(无状态、每次重发全量历史):会话 key = 消息前缀 哈希(到首个 assistant 轮为止)。客户端把 API 返回的 assistant 消息 原样回传时,哈希稳定命中 → 复用同一 HiAgent 会话,只发增量(最后一条 assistant 之后的消息),模型靠 HiAgent 服务端会话记忆(约百轮)续上下文。 若客户端改写/截断了 assistant 历史,哈希不命中 → 自动退化为新会话全量 重发(语义仍正确,只是多花 token)。首轮(消息里还没有 assistant 轮)也 会落映射——用「请求消息 + 合成回复」计算 key,回复恰好补上第一个 assistant 轮。
- responses
previous_response_id:这是 OpenAI 原生的会话链续机制, 按存储的会话 key 直接复用,与客户端回传什么无关(更稳)。实测跨轮记忆 正常。
会话身份隔离:DSH 用 sessionId 隔离会话(fork 铸造新 id,分支互不
串话);OpenAI 的等价钩子是请求里的可选字段 user。传了 user 的
客户端按 模型 + user 隔离会话 key——前缀相同的并行会话不会串到同一个
HiAgent 会话:
# 客户端 A / B 各自传自己的 user
curl ... -d '{"model":"hiagent-2","user":"session-A","messages":[...]}'
需要独立上下文的并行任务(agent 循环、多会话 UI)请务必传 user。
快速开始
# 复制示例配置并填入模型与 key
cp config.example.toml config.toml
# 运行
cargo run --release -- -c config.toml
# 或直接使用编译产物
cargo build --release && ./target/release/hiagent-proxy -c config.toml
首次运行(无 config.toml 且未指定 -c)会自动生成一份默认配置。
配置文件路径也可用环境变量 HIAGENT_PROXY_CONFIG 指定。
客户端接入(与 OpenAI SDK 相同):
curl http://127.0.0.1:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <下游 api key,未配置则省略>" \
-d '{"model":"hiagent-2","messages":[{"role":"user","content":"你好"}]}'
/v1/responses(含 previous_response_id 链续)、/v1/models、
GET /v1/responses/{id} 同样可用。
配置
见 config.example.toml(每个字段都有注释)。要点:
[hiagent]
base_url = "https://agent.tongji.edu.cn/api/proxy/api/v1/" # 必须以 / 结尾!
# 每个模型的独立 key(一个模型 = 一个 API key)
[hiagent.model_keys]
"hiagent-2" = "da0123456789abcdef012" # 注意:含点的模型 id 必须加引号!
"glm-5.2" = "da0123456789abcdef012" # 否则 TOML 会把 5.2 解析成嵌套表
[[models]]
id = "hiagent-2"
name = "DeepSeek V4 Flash"
protocol = "deepseek-dsml" # deepseek-dsml | glm-xml | glm-semixml
context_window = 128000
max_tokens = 4096
[[models]]
id = "glm-5.2"
name = "GLM-5.2"
protocol = "glm-xml"
[conversation]
no_reuse = true # 默认:每次新建会话 + 完整历史
max_reuse_turns = 100 # no_reuse=false 时:轮换阈值
retry_empty_reply = true # 空回复自动重试一次
配置项
| 字段 | 默认 | 说明 |
|---|---|---|
server.host / server.port |
127.0.0.1 / 8080 |
监听地址 |
server.api_keys |
[] |
下游 Bearer key 列表;空=开放 |
server.cors_origins |
["*"] |
CORS 允许的 Origin |
hiagent.base_url |
同济网关 | 必须以 / 结尾(端点是拼接的) |
hiagent.user_id |
default_user |
HiAgent 用户 ID |
hiagent.api_key / api_key_env |
— | 全局回退上游 key |
hiagent.model_keys |
{} |
按模型 id 的 key 字典(最高优先) |
hiagent.model_id / display_name |
hiagent / HiAgent |
未配置 models 时的默认模型 |
models[].api_key / api_key_env |
— | 行内 key(低于 model_keys) |
models[].protocol |
deepseek-dsml |
工具协议(六种:deepseek-dsml / json-toolcall / glm-xml / glm-semixml / kimi-xtml / infogather);未知 id 回退 deepseek-dsml |
models[].format |
"" |
仅 protocol = "infogather" 用:内层原生格式 id(缺省 deepseek-dsml),与 protocol 共同构成整体协议 |
models[].context_window / max_tokens |
128000 / 4096 |
元数据 + /v1/models 展示 |
models[].no_reuse / max_reuse_turns |
继承顶层 | 逐模型覆盖会话行为 |
conversation.no_reuse |
true |
每次新建会话(消除 ~3 轮窗口锁死) |
conversation.max_reuse_turns |
100 |
复用轮数上限(0=关闭轮换) |
conversation.store_path |
hiagent-conversations.json |
会话映射持久化(相对配置目录) |
conversation.max_initial_* |
120 / 100000 / 800000 |
首条提示防御上限(0=不限) |
conversation.retry_empty_reply |
true |
空回复自动重试一次 |
conversation.idle_timeout_secs |
180 |
流式空闲超时 |
上游连接方式(与 dsh-llm-hiagent 一致)
POST {base}create_conversation,请求头Apikey: <key>,body{UserID, Inputs: {var: "variable"}}→Conversation.AppConversationIDPOST {base}chat_query_v2,请求头Apikey+Accept: text/event-stream, body{UserID, AppConversationID, Query, ResponseMode: "streaming"}→ Dify 风格 SSEPOST {base}delete_conversation:尽力而为的会话清理(轮换 / 过期 / noReuse 换新前)- SSE 事件按事件类型路由:
think_message→ 思考增量;message/agent_message/text的answer→ 可见文本增量;message_cost→ token 计数;message_replace→ 重置累积;error→ 流内错误
工具调用走模型原生格式(DSML 或 GLM XML),模型输出后由协议层解析为
OpenAI tool_calls;历史里的 assistant 工具调用会序列化为原生格式(DSML/
XML)回注提示词,让历史成为可模仿的教师样例。
已知坑
base_url必须以/结尾,否则端点拼接 404。model_keys里含点的模型 id 必须加引号(如"glm-5.2" = "..."), 否则 TOML 把glm-5.2当成嵌套表(dotted key)。- 上游可能返回「失败兜底文案」(如
模型无法理解你的问题或回复内容为空): 代理检测到后自动换新会话重试一次,仍失败则以empty_response错误返回。 这是模型侧行为,代理只保证提示词完整(同 dsh-llm-hiagent 已知坑 #10/#13)。 - GLM 系 agent 需要合适的 agent 配置;若某 agent 对任何输入都回兜底文案, 是上游问题,与代理无关。
n>1不受支持(HiAgent 每次请求单回答);图片输入不支持(纯文本模型)。
测试
cargo test # 单元测试:DSML/GLM 解析、masker、提示词构建、配置、SSE 帧
cargo clippy # 无警告
cargo fmt --check
架构
src/
├── main.rs # 入口:日志、配置加载、运行时
├── lib.rs # 模块树 + 重导出
├── config.rs # TOML 配置(models + model_keys + 会话行为)
├── error.rs # ProxyError -> OpenAI 错误 JSON
├── model.rs # 模型注册表:id -> key / 协议 / 限制
├── hiagent.rs # HiAgent 客户端:会话创建/删除、chat_query_v2 SSE 解析
├── prompt.rs # OpenAI 消息 -> HiAgent 文本(历史框架、anti-self-Q&A)
├── conv.rs # 会话复用映射持久化
├── protocol/ # 工具协议:dsml.rs / glm.rs / masker.rs / common.rs
├── openai/ # types.rs(线上类型)、mod.rs(执行管线)、chat.rs、
│ # responses.rs(+ ResponseStore)、sse.rs
└── server.rs # axum 路由、下游鉴权、CORS、优雅退出
执行管线(openai/mod.rs,从 adapter.stream() 移植)对 chat 与 responses
共用:会话定位/创建 → 提示词构建 → SSE 流式消费(masker + think 路由)→
权威解析 → 会话过期/空回复重试 → 复用映射持久化。