OpenWork 与 LiteLLM 集成:零依赖 per-member 虚拟密钥对账方案实战
2026/9/13 4:38:52 网站建设 项目流程

OpenWork 与 LiteLLM 集成:零依赖 per-member 虚拟密钥对账方案实战

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

本文基于 OpenWork 仓库中examples/litellm-per-member-keys示例展开。该示例是一个零依赖的 Node.js provisioner(对账器),用于将 OpenWork Cloud 的 per-member LLM 凭据绑定与 LiteLLM 虚拟密钥(virtual key)体系进行双向同步,解决"一个上游密钥被多成员共享"的安全问题。读完本文你将掌握:如何配置该 provisioner 的环境变量、理解reconcileoffboard两条命令背后的完整调用链与 fail-closed 校验逻辑,以及如何用一条命令拉起完整的本地验证环境(桌面客户端 + 隔离的 Den 组织 + 数据库版 LiteLLM 网关)。

背景:为什么需要 per-member 凭据绑定

在组织场景中,管理员往往只持有一个上游 LLM 网关(如 LiteLLM)的接入密钥。若把这个共享密钥直接配给所有成员,将无法审计"谁在用"、也无法单独吊销"某个人"的访问权。OpenWork 的解决方案是per-member 凭据绑定:为组织中的一个托管 LLM Provider 声明credentialMode: "per_member",由外部网关(例如 LiteLLM)为每个成员签发独立的虚拟密钥,OpenWork 侧只保存成员与外部凭据 ID 的绑定关系。

这一点在 OpenWork 的官方 API 契约文档 per-member-llm-credentials.mdx 中有明确说明:

  • shared:仅保存一个 Provider 凭据,所有被授权成员通过 connect 路由获得同一份凭据;
  • per_member:为调用方组织成员解析各自独立的绑定,Provider 可以在绑定存在之前就完成授权。

需要特别强调的是:LiteLLM 的密钥铸造与元数据发现能力并未内建于 Den。官方文档明确指出"LiteLLM provisioning and metadata discovery are not built into Den",因此组织需要自行运行一个 provisioner,把它对接在 Den 的通用 Provider/凭据绑定 API 与 LiteLLM 管理 API 之间——这正是本文主角examples/litellm-per-member-keys/provision.mjs的定位:示例级对账器,而非 Den 的原生 LiteLLM 集成

环境变量配置

在仓库根目录examples/litellm-per-member-keys下,provision.mjs通过configFromEnv()(源码 provision.mjs)从环境读取全部配置。下表逐项说明含义:

环境变量含义源码中的处理
OPENWORK_DEN_API_URLDen API 基础地址cleanBaseUrl去除尾部斜杠(provision.mjs)
OPENWORK_DEN_TOKEN组织 owner 或 admin 的 Bearer Token作为authorization头发送
OPENWORK_ORG_ID组织 ID作为x-openwork-org-id请求头发送(provision.mjs)
OPENWORK_LLM_PROVIDER_IDper-member LLM Provider 的 ID用于 URL 路径编码encodeURIComponent
LITELLM_BASE_URLLiteLLM 基础地址,带或不带/v1均可通过liteLlmAdminBaseUrl剥离末尾的/v1(provision.mjs)
LITELLM_MASTER_KEYLiteLLM master key用于所有 LiteLLM 管理接口的 Bearer 认证
LITELLM_MODELS逗号分隔的模型 ID 列表经去重、trim 后作为每个虚拟密钥的授权模型集合(provision.mjs)

配置完成后,即可触发对账:

node provision.mjs reconcile

该脚本是零依赖的:只用 Node.js 内建能力(fetchnode:url),通过// @ts-check加 JSDoc 类型注释获得静态检查,没有任何npm install步骤。

reconcile 的两阶段流程

reconcile命令内部调用reconcileMemberKeys()(provision.mjs),分两个阶段执行:先同步模型元数据,再铸造缺失的成员密钥

阶段一:Provider 模型元数据同步

syncProviderModelMetadata()(provision.mjs)的执行顺序:

  1. 调用GET {denApiUrl}/v1/llm-providers?scope=manageable获取可管理的 Provider 列表,并校验目标 Provider:
    • 必须存在(providerId was not found);
    • source必须为custom
    • credentialMode必须为per_member(对应源码 manageableProvider)。
  2. 使用 master key 调用 LiteLLMGET /model_group/info拉取模型组元数据。
  3. LITELLM_MODELS中的每个模型要求精确的model_group精确匹配,且max_input_tokensmax_output_tokens必须是有限的正数(对应 liteLlmModelMetadata 中的requirePositiveNumber)。若 LiteLLM 漏掉了某个请求的模型、或缺少任一限额,在创建任何成员密钥之前就 fail closed(失败即中止)——示例从不猜测 token 限额,也不回退到通用值。
  4. 将 LiteLLM 提供的能力事实映射到 Den 的模型字段:
LiteLLM 元数据字段Den 模型字段说明
max_input_tokenslimit.context/limit.input上下文与输入 token 上限
max_output_tokenslimit.output输出 token 上限
supports_function_callingtool_call函数调用能力
supports_reasoningreasoning推理能力
supports_visionattachment视觉/附件能力
supports_response_schemastructured_output结构化输出能力
supported_openai_paramstemperaturetemperature温度参数支持

该映射逻辑集中在 synchronizedModelConfig:只有 LiteLLM 明确给出布尔事实时才写入对应字段(typeof metadata.facts.supports_function_calling === "boolean"),绝不臆造。

  1. 构造"当前配置"与"期望配置"两份快照后,用canonicalJson(对对象键递归排序,provision.mjs)做规范化比较;仅当确实发生变化时才向 Den 发送PATCH /v1/llm-providers/:id,否则返回{ action: "unchanged" },避免无意义写入。

这个"全量替换式 PATCH"有一个关键安全细节:请求体中不包含apiKey/apiKeys字段,因此 Den 会保留已落库的只写凭据(write-only stored credential),不会在元数据同步时被覆盖或泄露。

阶段二:铸造缺失的成员密钥

元数据同步完成后,脚本调用GET /v1/llm-providers/:id/member-credentials列出所有被授权成员绑定的凭据状态,对每个state === "missing"的成员:

  1. 生成 key alias:openwork-${orgMembershipId},并携带metadata.openwork_org_membership_id,调用 LiteLLMPOST /key/generate铸造虚拟密钥;
  2. 要求响应中必须同时包含key(明文密钥)与token_id(externalCredentialId 中强制校验,缺失则拒绝落库);
  3. 调用 DenPUT /v1/llm-providers/:id/member-credentials/:orgMembershipId,写入{ apiKey, externalCredentialId, externalPrincipalId? }

值得注意:LiteLLM v1.97 返回的token_id可以不保留明文密钥即可寻址该虚拟密钥。示例将token_id存入 Den 的externalCredentialId,之后管理员列表接口可以把该标识安全地回传给 provisioner——这正是 offboard 阶段不需要明文密钥的前提。

最终输出摘要中只包含元数据动作(updated/unchanged)与安全的模型限额、以及新铸造凭据的externalCredentialId绝不打印成员密钥本身provision.mjs中多处通过redact()(provision.mjs)在错误信息里对denTokenliteLlmMasterKey、成员apiKey等敏感串做[REDACTED]脱敏,且所有请求统一走 30 秒超时的requestJson(provision.mjs),错误文本截断为 1000 字符。

Den 侧 API 契约速览

为读懂 provisioner 的每一步,这里补上 Den 侧契约(详见 per-member-llm-credentials.mdx):

成员视角(只能管理自己的只写凭据)

  • PUT /v1/llm-providers/:id/my-credential:写入自己的凭据,body 只能是{ "apiKey": "..." }{ "apiKeys": { "ENV_NAME": "..." } }二者之一;
  • DELETE /v1/llm-providers/:id/my-credential:删除自己的凭据;
  • GET /v1/llm-providers/:id/connect:获取 Provider 配置与解析后的凭据。即使绑定缺失也返回 HTTP 200,凭据字段为 null,并附带memberCredential.state

管理员/Provisioner 视角(中央对账)

  • GET /v1/llm-providers/:id/member-credentials:列出每个被授权成员绑定的状态、版本与外部标识,永不返回凭据明文
  • PUT /v1/llm-providers/:id/member-credentials/:orgMembershipId:写入某成员的凭据,body 支持apiKey/apiKeys,可选externalPrincipalIdexternalCredentialIdexpectedVersion
  • POST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block:将既有绑定标记为 blocked。

其中memberCredential.state取值包括missingactiveblockedstaleerror。两个并发相关的语义:expectedVersion用于多 provisioner worker 场景,版本不匹配时返回 HTTP 409{ "error": "version_conflict" };被 block 的绑定由管理员持有,成员无法覆盖或删除,成员侧写/删请求返回 HTTP 409{ "error": "credential_blocked" },管理员PUT是显式的解封与替换路径。

offboard:必须先吊销上游,再标记本地

撤下某成员时,顺序规则是:先 block 上游 LiteLLM 密钥,再标记 Den 绑定为 blocked。原因在于:若上游吊销失败,应让 Den 绑定保持 active,使失败可见、可重试,而不是制造一个"本地已封禁"的假象。

node provision.mjs offboard member_...

offboardMember()(provision.mjs)执行的顺序:

  1. GET /v1/llm-providers/:id/member-credentials读取该成员的externalCredentialId(即 LiteLLM 的token_id);若不存在则直接报错拒绝执行;
  2. 调用 LiteLLMPOST /key/block,body 为{ "key": credentialId },并确认成功——LiteLLM v1.97 接受其生成的token_id,因此 offboard 阶段不需要成员密钥的明文;
  3. 调用 DenPOST /v1/llm-providers/:id/member-credentials/:orgMembershipId/block标记本地绑定;
  4. 验证成员 connect 请求返回 HTTP 200、凭据为 null 且memberCredential.state: "blocked"

这一顺序也被 per-member-llm-credentials.mdx 文档作为官方推荐流程列出。

一键拉起完整本地验证环境

README 提供了一个"可运行的整体世界",用于手工演练完整闭环(桌面客户端 + 隔离 Den 组织 + 数据库版 LiteLLM 网关,且 Provider 与成员密钥已预先对账完成)。从仓库根目录执行:

pnpm world up ./worlds/litellm-per-member.ts

启动后会打印:Den 的 URL、LiteLLM URL、同步后的模型限额、Den Provider 记录 ID、以及桌面客户端的 CDP URL。测试期间保持运行,结束后按 Ctrl-C 拆除整个环境(world 的 teardown 会自动清理)。

前置条件:Docker、本地 MySQL、本地 Redis。该 world 的网关使用确定性的本地 OpenAI 兼容 witness(witness),不会读取也不需要OPENAI_API_KEY

从源码看,这个 world 的实现位于 worlds/litellm-per-member.ts:它启动一个数据库版 LiteLLM(database: true,对应 evals/packages/env/src/litellm.ts 中以ghcr.io/berriai/litellm:v1.97.0镜像拉起网关与postgres:16-alpine的流程),创建名为LiteLLM Per-Member World的组织、管理员与成员 Alice,用liteLlmPerMemberProvider(evals/packages/env/src/litellm-provider.ts)完成 Provider 创建 + 元数据对账,并以管理员身份登录桌面客户端、将模型指向{provider}/{model},最后输出 Alice 的 per-member 虚拟密钥、master key、upstream key 等凭证供手工验证。

这里有一个值得玩味的工程细节:witness 网关会校验虚拟密钥的指纹。在 litellm.ts 的 witness 中,每个上游请求都记录了tokenId(对携带的 Bearer token 做 SHA-256),只有与upstreamTokenId指纹匹配的请求才会返回 200——这意味着任何未走 Den 下发、非法铸造的密钥都会在网关层直接被拒,整条链路可观测、可审计。这为验证"每成员一密钥"是否真正生效提供了最直接的证据。

从源码看对账器的设计要点

  • 保守的 fail-closed 哲学:从requirePositiveNumberrequireString到"模型必须精确匹配 model_group 且限额必须是有限正数",任何元数据缺口都会让对账在铸造密钥前整体中止,绝不回退猜测值;
  • 最小写、保现状:Den PATCH 仅在规范化 JSON 比较后确有变化才发出,且全量替换时省略apiKey/apiKeys,保留 Provider 配置、模型名与未知字段、当前成员/团队授权不变,只更新 token 限额与能力字段;
  • 密钥只在最短窗口内出现:明文apiKey仅在铸造响应与写入 Den 的请求之间停留,随后只以token_idexternalCredentialId)继续流转,错误信息全程脱敏;
  • 与 Den 核心解耦/model_group/info调用与字段映射被刻意实现为 LiteLLM 专属的示例逻辑,而 Den 的 Provider PATCH 与 per-member 凭据 API 只接收通用模型配置,不硬编码任何 LiteLLM 行为,从而保持厂商中立。

延伸阅读

  • API 契约与成员/管理员双视角流程:per-member-llm-credentials.mdx
  • 示例完整实现:provision.mjs
  • 本地 world 编排:worlds/litellm-per-member.ts
  • 测试环境封装(witness 网关与 Provider 对账):evals/packages/env/src/litellm.ts、evals/packages/env/src/litellm-provider.ts

该集成在 OpenWork 的 eval 体系中作为可执行证明存在:evals/packages/env/src/litellm-provider.ts会直接import示例模块并调用reconcileMemberKeys,验证其返回的元数据动作与模型限额符合预期,同时该世界也登记在evals/specs/shared-world-engine.test.ts的 world 清单中,可纳入自动化回归。

【免费下载链接】openworkThe open-source alternative to Claude Cowork (powered by opencode)项目地址: https://gitcode.com/GitHub_Trending/ope/openwork

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询