Skip to content

Provider

OryxOS 把 Spring AI Alibaba 用作薄薄的协议适配层——它处理各厂商的 wire format 差异(OpenAI、Anthropic、Gemini 等),OryxOS 本身不需要关心这些。在此之上,ProviderService 负责路由、审计,以及让多 Provider 可靠运行的显式名称到模型的映射。

Provider 是动态的

Provider 不再是启动时固定不变的映射表。它现在是存储在 SQLite(providers 表)里、可运行时增删改的记录,通过 /api/v1/providers REST 端点或管理台的 Provider 页面管理。无需重启 OryxOS 即可新增、编辑、删除 Provider。

一条 Provider 记录有四个字段:

字段含义
name全局唯一 key,Agent 在自己的 AGENT.md 里按它引用
apiKey端点凭证。按设计以明文存储、明文返回(见下文)
baseUrlOpenAI 兼容端点的 base URL。除 mock 外每个 Provider 都必填
description自由文本备注

保留名 mock 是内置的假模型——不需要 apiKey 也不需要 baseUrl,用于无 key 的全链路自测。其余(非 mock)Provider 都必须填 base URL

启动播种——从配置写入数据库

启动时 OryxOS 读取 application.yml 里的 oryxos.providers 列表,并把每一条播种providers 表——仅当同名 Provider 尚不存在时才写入。播种之后,数据库是唯一真相源:修改配置里的 oryxos.providers 不会覆盖已经在 DB 里的 Provider。请通过 API 或管理台管理线上 Provider,而不是改配置。

yaml
# application.yml —— 仅在首次启动时播种进 DB
oryxos:
  providers:
    - name: deepseek
      api-key: ${DEEPSEEK_API_KEY}       # 环境变量占位符,不明文写死
      base-url: https://api.deepseek.com
    - name: kimi
      api-key: ${KIMI_API_KEY}
      base-url: https://api.moonshot.cn/v1
    - name: mock                          # 内置假模型,无需 key/url

API key 来自配置里的环境变量占位符(${...})——不得明文写死。注意 Spring 对列表型配置是整体替换而非合并,所以外部覆盖 oryxos.providers 会替换掉整个列表。

通过 REST 管理 Provider

所有端点都在 /api/v1 前缀下,返回体统一包裹在 { code, message, data, timestamp } 信封里(code0 表示成功)。

方法路径用途
POST/api/v1/providers新建(nameapiKeybaseUrldescription);重名/空名返回 400;非 mock 必须填 base-url
GET/api/v1/providers列出全部 Provider
GET/api/v1/providers/{name}查询单个(不存在返回 404
PUT/api/v1/providers/{name}更新(apiKeybaseUrldescription
DELETE/api/v1/providers/{name}删除

列表和单查响应(ProviderView)返回 nameapiKeybaseUrldescription。其中 apiKey 以明文返回——这是内网核心阶段的有意选择,管理台需要展示和编辑它。不要把这些端点暴露到不可信网络。

Provider 路由

每个 Agent 的 AGENT.md frontmatter 按名称声明恰好一个 Provider。当 ReActLoop 需要调用 LLM 时,ProviderService 按名称在注册表里查找该 Provider 并解析出它的 ChatModel

yaml
# .oryxos/agents/ops-agent/AGENT.md(frontmatter)
provider:
  name: deepseek          # 与某条 Provider 记录的 name 对应
  model: deepseek-chat
  temperature: 0.7

一个 OryxOS 实例可以同时运行使用不同 Provider 的 Agent。一个 Agent 用 deepseek,另一个用 anthropic——Agent 的 frontmatter 就是路由表条目。

动态解析与缓存

Provider 的 ChatModel 按需从注册表记录构建,并以 name|apiKey|baseUrl 为 key 缓存。因为缓存 key 包含了凭证和 URL,改动某个 Provider 的 key 或 base URL 就会换掉缓存 key,于是模型在下次调用时重建——一次 CRUD 编辑立即生效,无需重启。mock 名解析到内置假模型;其余都通过 OpenAiApi(baseUrl, apiKey) 构建成 OpenAI 兼容客户端。

为什么显式映射,而不是类型扫描

宪法原则三依然成立:路由是显式的名称到 ChatModel 映射,绝不从 Spring Bean 类型推断。唯一的变化是这张映射表现在是运行时可变的——由 SQLite 支撑的 ProviderRegistry——而不是启动时冻结的静态 map。

类型扫描失败的原因没变。每个 Provider 的模型 Java 类型都相同(ChatModel / OpenAiChatModel),你无法通过检查 Bean 类型区分 deepseekqwen

java
// 正确 —— 按名从注册表解析,按 (name|apiKey|baseUrl) 构建 + 缓存
ProviderDef def = registry.find(providerName).orElseThrow(...);
ChatModel model = resolveModel(def); // 缓存;key/url 变化时重建

// 错误 —— 类型扫描无法区分各个 Provider
applicationContext.getBeansOfType(ChatModel.class); // 全都长一样

这就是为什么 Agent frontmatter 里的 Provider 名称必须与某条 Provider 记录的 name 完全一致。

审计

每次 LLM 调用——无论成功还是失败——在返回给 ReActLoop 之前都会向 SQLite 的 llm_calls 表写入一条记录,字段包括:providermodelprompt_tokenscompletion_tokenstotal_tokensduration_ms。token 数量来自 ChatResponse.getMetadata().getUsage()

这是无条件执行的,没有配置开关可以关闭。

规划中的功能

以下内容不在核心阶段:

  • 故障转移链 — 先试 Provider A,超时或报错时回退到 Provider B
  • 竞速调用 — 同时向两个 Provider 发送请求,用先返回的那个
  • 熔断器 — 自动停止向持续报错的 Provider 路由
  • 基于成本的路由 — 自动把廉价查询路由到更便宜的模型
  • 单次请求级 Provider 覆盖 — 单次 API 调用临时覆盖 Agent 配置的 Provider