【免费下载链接】ObsidianArc
A self-hosted AI platform with a Go server, multi-provider routing, usage tracking and an admin console, with chat and agentic work modes.
Obsidian Arc(ObsidianArc)是一个自托管的 AI 平台:Go 编写的服务端统一管理多家模型服务商,提供模型目录、用量计费、管理控制台,以及聊天和 Agent 工作模式。这篇文章带你用 3 个核心机制看懂它的多服务商模型路由实现原理——模型映射(route_to)、api_name别名,以及流式推理解析,全部基于仓库中的真实源码位置,新手也能跟上。
三个机制,一分钟建立整体认知 🧭
| 机制 | 解决的问题 | 关键代码 |
|---|---|---|
模型映射route_to_id | 用户看到的模型 ≠ 实际执行的模型 | internal/model/model.go |
api_name别名 | 对外提供一个稳定好记的 API 名称 | internal/model/model.go |
| 流式推理解析 | 把 OpenAI / Anthropic 两种"思考"格式统一成一种事件流 | internal/adapter/adapter.go |
理解这三个机制,基本就理解了这个平台"多家服务商、一套对外接口"的设计核心。
模型映射:一个名称背后站着另一个模型
在模型目录里,每个模型记录同时保存了两种身份:
ModelID:上游服务商真正认识的模型名,只展示给管理员;RouteToID(route_to_id):当请求应该被"转交"给另一个模型执行时填的目标,普通情况下留空。
见 Model 结构定义 中RouteToID字段旁的注释:这是一个"管理细节",对外接口从不携带它。
真正执行解析的是 Authorize 方法。它返回一个三元组 Resolved:
Resolved{ Model // 用户请求的那个模型(权限、计费、展示都以它为准) Upstream // 实际发出请求的模型(没有路由时两者相同) Provider // 目标模型所在服务商的凭据 }这里有几个值得注意的工程决策,源码注释里写得直白:
- 只允许单跳:A 路由到 B 可以,B 再路由到 C 会被拒绝(
ErrRouteChain)。因为解析只做一跳,链式路由会让请求"走错"。校验逻辑见 checkRouteTx,并发下也不会形成链条(route_chain_test.go)。 - 可以跨服务商:把 A 服务商上出问题的模型路由到 B 服务商的备用模型,此时请求用的是目标模型服务商的凭据,测试覆盖见 routing_test.go。
- 目标被禁用就直接失败:不会悄悄退回用户原本请求的模型——那个模型往往正是出问题的原因(TestRouteToADisabledModelIsRefused)。
- 目标正在被删时不能建立路由:写路由和删模型持有同一把行锁,二者不可能都成功(lockRouteTarget)。
- 用量和权限始终记在"用户选的"模型上:客户端选择器、对话记录、计费账本里看到的都是请求时的模型,用户感知不到背后换了人(routing_test.go 的注释 把这个承诺写得很清楚)。
官方文档对使用边界也有一句话提醒:变更前要确认目标模型的启用状态、权限和能力,不同模型的上下文和推理参数并不保证一致,参见 docs/admin/providers-and-models.md。
api_name 别名:对外一个名字,对内仍是上游真名 🏷️
api_name是管理员为模型另起的"外部 API 名称"。它的语义在 Model 字段注释 中定义得很精确:
- 它决定 OpenAI 兼容接口对外宣称并接收什么名字;
- 但请求发往上游时仍然使用
ModelID——别名只改变边缘的名字,够不到服务商那边。
围绕这个别名,源码里有几层细致的设计:
1. 名称归一:PublicName 很简单——设了api_name就用它,否则就用上游模型 ID。而 APIRef 处理重名:当两个模型共用同一个上游 ID(典型场景:一个模型配了主服务商和备用服务商两条记录)且都没有别名时,对外名称会追加行 ID 的最后 6 位作为后缀,保证每个名字都能唯一落地。
2. 唯一性与格式:两个模型不能抢同一个api_name(唯一索引,冲突时返回api_name_taken);带空格、/、?这类在 URL 和配置文件里活不下去的名字会被直接拒绝(ErrInvalidAPIName)。测试见 api_name_test.go。
3. 解析优先级:客户端按名字找模型时,Tier 排序 保证"输入的名字"优先于"展示标签"——即使某条记录的展示名称恰好也叫claude,真正的api_name持有者也会胜出(TestAPINameBeatsADisplayNameMatch)。
4. 对用户隐藏:普通用户拉取模型列表时,返回的 Public 结构 刻意不含上游模型 ID 和服务商名——"回答实际由哪家公司提供"正是这套间接层想要藏住的信息,从结构体层面就杜绝了泄露,而不是在每个调用点偷偷隐藏。
流式推理解析:两种协议的"思考"统一成一种事件 ⚡
平台内部只承认四种事件:正文增量(EventDelta)、推理增量(EventReasoning)、用量更新(EventUsage)、完整工具调用(EventToolCall),定义见 EventType 常量。所有协议差异都被 internal/adapter 包挡在下面,上层永远看不到if kind == "openai"这类分支。
推理风格:配置而不是代码分支
同一个 OpenAI 协议的不同服务器,"告诉模型去思考"的方式并不一样。所以推理风格是按服务商配置的值,而不是写死的代码,见 ReasoningStyle:auto(按协议默认)、none、anthropic(thinking + budget_tokens)、openai_effort(reasoning_effort)、openrouter、qwen(enable_thinking)。模型一级的设置还能覆盖服务商一级的默认值(Authorize 中的处理)。
OpenAI 流:推理有两条到达路径
OpenAI 兼容流的解析在 openAIAdapter.readStream。推理内容可能从两个地方来:
- 专用字段
reasoning/reasoning_content:已经分离好,直接转发为EventReasoning; - 行内标签
<think>…</think>:一些没有推理字段的服务器会把它混在正文里。由于标签可能横跨两个数据块,实现要在每个增量上都从完整缓冲区重新切分,并用计数器(emittedAnswer/emittedInline)保证每个事件只携带新增部分。
这里有个很见功力的细节:末尾的<th还不能当正文发出去——它可能是<think>标签的开头。先扣住它等下一个数据块,否则用户会先看到一段多余的<th,再看到它"消失"。
Anthropic 流:思考块与分头报告的用量
Anthropic 的解析在 anthropicAdapter.readStream:
thinking增量直接映射为EventReasoning,与正文天然分离;- 用量分两头报告:输入 token 在消息开始时给出,输出 token 在结束时给出,所以用 Usage.Merge 逐字段合并而不是覆盖,否则"简单覆盖"会丢掉一半数字;
- 结束语义:以
message_stop或stop_reason作为"回答完整"的依据,连接被中途掐断不能算完成; - 思考预算还有 ClampThinkingBudget 保护:预算不能把回答抬过管理员设置的上限,天花板太小时干脆关闭思考。
推理档位:模型可以自定义"思考多少"
前端只发送一个统一的推理强度,它不知道budget_tokens和reasoning_effort的存在。管理员还可以为单个模型定义自己的档位列表(reasoning_tiers),ResolveTier 负责把用户的选择解析成实际的强度与预算——注意它的容错哲学:遇到过期或未知的档位,落到中间档而不是报错拒绝,因为浏览器里保存的偏好可能早于管理员上次改列表。
普通用户视角:这些机制意味着什么 ✅
作为使用方,你通常不需要碰这些细节,但了解它们有助于看懂管理台和排障:
- 在
/admin/models里配置模型时,api_name留空即直接以上游 ID 对外提供;填上别名后,OpenAI 兼容客户端改一个配置就能切换模型,详见 官方文档:服务商与模型; - 某家服务商抽风时,管理员可以把一个对外模型路由到备用服务商执行,用户侧无感知;
- 看到推理内容在界面上单独成段,背后就是"思考事件与正文事件分离"这套机制在起作用。
小结
Obsidian Arc 的多服务商模型路由由三个相互独立的机制组成:route_to_id 单跳映射解决"谁实际执行"(权限与计费不变、跨服务商、禁用即失败);api_name 别名解决"对外叫什么名字"(上游真名照发、唯一性有索引保证、用户列表隐藏实现细节);流式推理解析解决"思考怎么到达"(四种统一事件、按服务商配置推理风格、推理预算有天花板)。三者都集中在 internal/model 与 internal/adapter 两个包中,配合 routing_test.go、api_name_test.go 等测试,把"用户感知不到这些机制"这一核心承诺固定了下来。
【免费下载链接】ObsidianArc
A self-hosted AI platform with a Go server, multi-provider routing, usage tracking and an admin console, with chat and agentic work modes.
相关推荐
kiss-translator:免费开源的双语对照翻译扩展,让外文网页一键变中英对照读物
kiss translator:免费开源的双语对照翻译扩展,让外文网页一键变中英对照读物 一周前的周日下午 23:15,我坐在家里书桌前,盯着一篇读两遍都没读完
前端如何为 Hermes-CN-Desktop 配置模型:主流国内云模型服务商完整清单
如何为 Hermes CN Desktop 配置模型:主流国内云模型服务商完整清单 Hermes CN Desktop 是 Hermes Agent 中文社区推
人工智能AI Agent交互助手代码智能体桌面应用MCP Clients本地部署开发工具模型不答先别换模型:dsh-purge 把破甲、规则集、防线体检装进 DSH 侧栏
模型不答先别换模型:dsh purge 把破甲、规则集、防线体检装进 DSH 侧栏 模型不答的时候,你分不清是模型犯倔,还是宿主审批门拦的。dsh purge
dsh-plugin网络安全AI 安全治理模型安全红蓝对抗人工智能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考