首页
/ PostHog 双 LLM 网关对等性审计:以 SKILL 驱动的 Python/Go 网关契约核验与 PARITY.md 更新实战

PostHog 双 LLM 网关对等性审计:以 SKILL 驱动的 Python/Go 网关契约核验与 PARITY.md 更新实战

2026-09-06 19:32:59作者:舒璇辛Bertina

本文基于 PostHog 仓库中的 auditing-llm-gateway-parity 技能文档,讲解如何系统性地审计 PostHog 的 Python 网关(services/llm-gateway)与 Go 网关(外部仓库 PostHog/ai-gateway)之间的能力对等性:如何固化两侧代码基线、如何按契约逐项取证、如何把证据分类为"已支持/阻塞/需验证",以及如何维护 PARITY.md 这份迁移决策记录。读完本文,你将在任一网关发生认证、归因、计费、路由或元数据变更时,能独立完成一次不引入虚构结论的对等性审计,并产出可复用的迁移决策依据。

一、为什么需要"对等性审计"这件事

PostHog 当前同时运行两个 LLM 网关:

  • Python 网关:本仓库的 services/llm-gateway,一个独立的 FastAPI 微服务,代理 Anthropic、OpenAI、OpenRouter、Fireworks AI 等推理 API,并内建产品级鉴权、限流、成本预算与 $ai_generation 事件归因;
  • Go 网关:独立仓库 PostHog/ai-gateway,按 PARITY.md 的迁移政策,新调用方和新功能应默认选用 Go 网关。

PARITY.md 明确规定了迁移政策:services/llm-gateway 处于"非正式代码冻结"状态,任何修改 Python 网关的 PR 必须写明受阻塞的调用方、Go 网关的确切能力缺口,以及为什么该变更无法等待或落到 Go 网关。而这份文档本身的准确性,正是由 auditing-llm-gateway-parity 这个技能(Agent Skill)所定义的流程来保证的。

该技能文档的 frontmatter 描述给出了触发时机:当任一网关变更了认证、归因、计费、端点、提供商、模型、路由或元数据时;当评审 Python 网关的变更时;或当需要刷新、核验、汇报网关对等性时。技能的核心约束是:审计只更新对等性记录(parity record),不迁移调用方——调用方迁移由另一个独立技能 /migrating-llm-gateway-callers 负责。

二、记录源码基线:审计的第一步是"锁定证据边界"

技能文档要求审计前先记录双方的源码版本,这是整篇审计结论的可信度基础:

  1. PostHog 侧基线:fetch origin/master,记录其 SHA 作为 PostHog 基线;
  2. 审计对象是当前工作树:包括相对该基线已提交和未提交的变更。技能文档特别强调——不能因为某个网关变更尚未合入 master 就忽略它。在途变更(in-flight change)同样是审计对象;
  3. Go 侧基线:用 gh api repos/PostHog/ai-gateway/commits/main 读取 PostHog/ai-gateway 的 main 分支 SHA,记录为 Go 基线;
  4. 在途 Go 变更的特殊处理:如果审计的是尚未合入的 Go 变更,应检查该 PR 分支或工作树相对 main 的状态。此时其契约只能视为"待定"(pending),不得记录为当前已支持
  5. 其余情况下,检查 PostHog/ai-gatewaymain 上的经过认证的检出。

一个关键的证据原则紧随其后:审计的是实现代码。README 文件和已有的对等性表格只是起点,不是证据。 这条规则直接决定了后文"逐项检查契约"必须读源码而不是抄文档。

当前 PARITY.md 尾部正是这套流程的产物示例:

Last verified on 2026-08-27 against:

  • PostHog/posthog working tree compared with master at 88997b515b337b2482814df60c132f60d5de5be4
  • PostHog/ai-gateway main at 37ee8bca725a13bca71e40b36f916765f625f939

三、Python 网关的证据采集点

技能文档列出了 Python 网关的六个取证位置,下面结合仓库源码逐一说明每个位置到底在看什么。

3.1 路由与线上行为:services/llm-gateway/src/llm_gateway/api/

入口 routes.py 是一个极简的 FastAPI APIRouter 组合层,只挂载了四个子路由:Anthropic、Models、OpenAI、Usage。

router = APIRouter()
router.include_router(anthropic_router, tags=["Anthropic"])
router.include_router(models_router, tags=["Models"])
router.include_router(openai_router, tags=["OpenAI"])
router.include_router(usage_router)

审计时这里的重点是路由名 + wire behavior(线上行为)POST /v1/chat/completionsPOST /v1/responses(OpenAI 兼容),POST /v1/messagesPOST /v1/messages/count_tokens(Anthropic 兼容),以及产品作用域路由 POST /{product}/v1/...README 还记录了 Bedrock 通过 X-PostHog-Provider: bedrock 头复用 Anthropic 端点、以及 X-PostHog-Use-Bedrock-Fallback: true 的 5xx 回退行为——这些"头驱动"的隐性契约正是 Go 网关是否对等时最容易被路由名掩盖的细节。

3.2 接受的凭据:services/llm-gateway/src/llm_gateway/auth/

authenticators.py 用两个具体认证器实现了凭据体系,是审计"认证契约"时必看的实现证据:

认证器 令牌前缀 数据库查询 范围要求
PersonalApiKeyAuthenticator phx_ posthog_personalapikey JOIN posthog_user,按 sha256$<hash> 匹配 secure_value 必须携带 llm_gateway:read scope,且通配符 scope 不满足该要求
OAuthAccessTokenAuthenticator pha_ posthog_oauthaccesstoken JOIN posthog_user,按 token_checksum 匹配 scope 检查允许通配符allow_wildcard=True),且额外校验令牌未过期、application_id 非空,并携带出 sandbox_task_idscoped_teams 等字段

两个认证器都是"纯数据库查询、无副作用"的抽象基类实现,成功结果按 auth_cache_ttl / auth_cache_ttl_oauth 分别缓存。对照 PARITY.md 的 Parity map 可以验证技能文档的分类逻辑:Go 网关支持 phs_ 项目密钥、pha_ OAuth 凭据以及铸造的 phe_ 作用域令牌,而 Python 侧独有的 phx_ 个人密钥、通配符 OAuth scope 处理、产品专属 API key/OAuth 应用规则、Desktop 按请求选择项目并对照 OAuth scope 与实时组织成员资格校验的能力,被记录为"可能阻塞迁移"的 Python-only 行为——这些差异正是从上面这份认证器源码里读出来的,而不是从 README 抄来的。

3.3 数据库读取面:db/required_tables.py

required_tables.py 声明了网关在 PostHog Postgres 中的最小权限读表白名单:

REQUIRED_TABLES: frozenset[str] = frozenset(
    {
        # Personal API key authentication
        "posthog_personalapikey",
        "posthog_user",
        # OAuth access token authentication
        "posthog_oauthaccesstoken",
        # Project-scope check
        "posthog_team",
        "posthog_organization",
        "posthog_organizationmembership",
        "ee_accesscontrol",
        "ee_role",
        "ee_rolemembership",
    }
)

技能文档对这个文件附加了一条运维前置约束:任何新增表读取的变更,都必须先在 posthog-cloud-infra 中为每一个环境落地 SELECT 授权,才能声明这张表。配套的 tests/test_required_tables.py 把这个声明与包内 SQL 绑定,/_readiness 端点则校验连接角色持有全部声明的授权——授权被吊销会让存量 Pod 直接变为不可服务。审计时检查这个文件,就能判断 Python 网关的鉴权/授权契约是否依赖了新增的表访问,而这通常是跨环境灰度部署中最容易出事的点。

3.4 可信产品策略、模型与计费:products/config.py

products/config.py 是技能文档点名的"trusted product policy, models, and billing"证据源。ProductConfig 数据类定义了每个产品的五个授权维度,审计时每个字段都对应一条可被 Go 网关缺失而阻塞迁移的契约:

  • allowed_application_ids:OAuth 应用白名单。为空集(默认)表示该产品不接受任何 OAuth 应用;只有显式列出应用 ID 才允许 OAuth 访问。例如 posthog_code 只接受 US/EU/dev 三个 PostHog Code 应用 ID。
  • allowed_models + exact_model_match:模型白名单。默认前缀匹配(以支持 Bedrock 区域/版本后缀 ID),exact_model_match=True 时改为精确匹配,防止 <pinned>-pro 之类的变体绕过模型定价。
  • allow_api_keys:如 posthog_codebackground_agentsonboardingcustom_image_scansconversations 等全部为 False,即 OAuth-only。
  • credit_bucket:计费契约的核心。CreditBucket.AI_CREDITSCreditBucket.POSTHOG_CODE_CREDITS 对应 Django 配额资源键;None 表示该产品不向客户计费,发出的 $ai_generation 事件会被打上 $ai_billable=false,用量报表(posthog/tasks/usage_report.py)忽略它们。
  • requires_server_credential:要求 OAuth 调用方出示服务器铸造的凭据(携带内部 scope internal_run:read)。注释解释了其防绕过意图——用户自己的 Desktop OAuth 令牌拿不到内部 scope,因此无法借共享产品路由绕开 posthog_code 的免费层级模型门。

同一文件还包含 RESTRICTED_MODEL_PRODUCTS(把 GLM 5.3、GLM 5.3 Flash、DeepSeek V4 Flash 等 Baseten 独占模型锁给 posthog_code/review_hog)、MODEL_ACCESS_FLAGS(模型级访问开关,如 zai-org/glm-5.3-flashposthog-code-glm-53-flash-model 标志)以及 resolve_cost_key(按令牌 scope 而非 URL 产品名选择计费预算——interactive_run:read scope 的 Signals 交互运行独立计入 signals_interactive 预算)。PARITY.md 中"PostHog Desktop、PostHog Code、云代理、onboarding、Wizard 暂留 Python 网关"的阻塞行,以及"Go 目录仅提供 GLM 5.2、glm-5.3-flash 是 Baseten 独占"的细节,均可在此文件中找到逐字对应的实现证据。

3.5 预算与限额:rate_limiting/

rate_limiting 目录 实现了三层成本限额(产品级、用户级、单次运行级)与拒绝事件、成本仪表发布器、Redis 令牌桶等。config.pyDEFAULT_PRODUCT_COST_LIMITS(默认 $1000/24h 产品级兜底)、DEFAULT_USER_COST_LIMITS(burst + sustained 双窗口)、DEFAULT_SANDBOX_TASK_COST_LIMITS(按令牌铸造时绑定的 sandbox_task_id 而非调用方提交内容计费的单次运行上限)三组默认值,构成 PARITY.md 里"Django plan or quota enforcement"阻塞行的具体来源——Go 侧支持滚动归因预算、团队硬上限、逐令牌上限,但计划检查、配额桶、免费模型策略与"不记任何钱包"的请求仍归 Python 管。

3.6 事件归因:callbacks/ 与 Django 侧调用点

callbacks/posthog.py 是归因契约的实现证据。_apply_owned_event_properties 强制重写网关自有属性——ai_product$ai_billable$ai_effort 以及 team_id——并且只有在个人 API key 且属员(staff)身份时才允许调用方覆盖 team_id,其余情况一律丢弃或强制为鉴权团队的 ID。这正是技能文档中"不要将调用方提供的遥测视为可信策略"一原则的源码注脚:ai_product 事件属性不能替代产品认证、授权与计费。同文件还处理了 $ai_input/$ai_output_choices 超过 capture 通道上限时的截断(_MAX_CAPTURE_SIZE = 800KB,远低于 Kafka 的 1MB 消息上限)。

Django 侧的调用点证据在 posthog/llm/gateway_client.pyget_llm_client 以及 PARITY.md 推荐的 build_openai_client / build_async_openai_client / build_async_anthropic_client 构建器,负责转发产品归因与调用方选定的元数据,并保留回滚期的 Python 回退。审计"必填契约"时必须同时看这些真实调用点,而不能只看网关自身的接口定义。

四、Go 网关的证据采集点

技能文档为 Go 网关列出了对称的取证清单(路径相对于 PostHog/ai-gateway 仓库):

关注契约 检查位置
API 形状 internal/httpapi/routes.go 与 dispatch 包
凭据与身份策略 internal/auth/internal/principal/
计费与限额 internal/httpapi/admission.gointernal/ledger/internal/quota/
模型、提供商、翻译与故障转移 internal/catalog/internal/router/internal/dispatch/
归因与元数据 internal/emitter/ 与请求解析
预期与延后契约 docs/product.md且必须与代码逐条核对

清单里刻意排除了 Go 网关的 README 与 PARITY.md 本身——与 Python 侧一致,文档只是起点。由于该仓库在本仓库之外,审计者需要在另一个检出中完成这些检查;技能文档要求的"经过认证的检出 + main SHA 记录"就是为了让这一侧的证据同样可复现。

五、审计的核心判定规则

技能文档给出三条最重要的判定规则,它们共同防止审计结论失真:

5.1 路由同名不等于行为对等

Check both request and response behavior. Matching route names do not prove parity for headers, streaming, errors, timeouts, retries, billing, or emitted events.

也就是说,即使 Go 网关有同名的 /v1/chat/completions,仍要分别验证:请求头处理、流式分块、错误格式(Python 侧遵循 OpenAI 错误 JSON 格式与 400/401/403/429/504 状态码表)、超时、重试、计费与发出的事件。PARITY.md 的 Parity map 中"Event metadata"一行正是此类判定结果:Go 侧统一为一个 X-PostHog-Properties JSON 头加专用头,而 Python 侧还有 X-POSTHOG-PROPERTY-*X-POSTHOG-FLAG-* 头,且 Python 的 per-key 属性头还能发出 $ai_session_id

5.2 调用方遥测不是可信策略

Do not treat caller-supplied telemetry as trusted policy. An ai_product event property does not replace product authentication, authorization, or billing.

对应 3.6 节的源码:Python 网关在事件属性合并完成后强制重写网关自有属性,Go 侧同理(其 Parity map 表述为"标准凭据头仍由调用方控制,不构成产品授权")。审计时必须确认 Go 侧的"网关自有"属性集合覆盖了 Python 侧的全部可信策略点。

5.3 "不计费"标志不是自动阻塞项

Do not treat Python's unbilled flag as an automatic blocker. An internal workload can move to Go with a PostHog-owned team credential when debiting that wallet is the intended way to attribute PostHog spend.

即:credit_bucket=None 的 Python 产品并不天然卡住迁移——当"从 PostHog 自有团队钱包扣费"本身就是归因 PostHog 内部支出的预期方式时,内部工作负载可以带着 PostHog 自有团队凭据迁往 Go。只有当调用方必须保留客户专属计费策略、或必须不记任何钱包时才构成阻塞。这条规则直接映射到 PARITY.md "Use the Go gateway" 表中"Server-to-server PostHog call"一行"Internal spend can use a PostHog-owned team wallet"的表述。

此外,每条差异都要至少指认一个受影响的使用场景类别,并删掉不影响迁移决策的细节——保证记录以决策为导向而非百科全书化。

六、证据三分类法

技能文档定义了三种且仅三种证据状态:

状态 含义 判定标准
Supported Go 网关满足该契约 实现代码证据齐全,请求/响应行为均核验
Blocking 阻塞迁移 现存使用场景将失去认证、可信归因、计费策略、API、提供商或线上行为
🔎 Verify 需验证 支持存在,但调用方需确认具体配置或行为

对照 PARITY.md 可以看到三分类的落地形态:

  • Use the Go gateway:客户 LLM 流量(phs_ 项目密钥/OAuth 凭据、团队钱包付费、标准 OpenAI/Anthropic API 即够用)、服务端对服务端调用、有上限的委托运行(phe_ 短令牌 + 固定归因 + 逐令牌消费上限)、标准 SDK 代理(含流式、幂等、模型目录)、网关托管路由(OpenAI/Anthropic/Azure OpenAI/Bedrock 及已配置的 Modal、Fireworks、Baseten 主机)。
  • Stay on the Python gateway for now:Desktop/PostHog Code/云代理/onboarding/Wizard 等第一方产品(Go 无法执行 Django 的 OAuth 应用白名单、项目访问检查、计划与计费策略);Django 计划/配额执行;用户按需启动的代理运行(需要 Python 的溯源策略);OpenRouter 与 Cloudflare Workers AI 提供商;OpenAI 语音转写;经 Anthropic Messages 使用 OpenAI 模型的反向翻译;Python 产品用量状态 API。
  • 🔎 Verify before switching:模型是否出现在 GET /v1/models、凭据解析出的团队/scope/吊销状态/上限、钱包是否已充值、X-PostHog-Distinct-Id 等归因头是否够用(注意 Go 不会从 OpenAI user、Anthropic metadata.user_id 或 OAuth 用户推导事件 distinct ID)、X-PostHog-Session-Id 会话归因、目标环境是否声明了 Fireworks/Baseten 主机与凭据(Baseten 还需子处理者批准)、路由/健康感知/严格 X-PostHog-Provider 固定策略是否匹配、线行为的字段/流式/错误/超时/重试、以及 Python 的 per-key 属性与标志头到 Go JSON 属性头的转换。

七、更新对等性记录:PARITY.md 的维护动作

审计结论的落盘对象是 services/llm-gateway/PARITY.md,技能文档规定四类更新动作:

  1. 移动使用场景:在 supported、blocked、verify 三个小节之间迁移场景条目;
  2. 增删 Parity map 中迁移相关的契约行(当前 map 覆盖 Authentication、Trusted first-party identity、Attribution、Billing and limits、OpenAI APIs、Anthropic APIs、Providers、Models、Routing and failure behavior、Event metadata、Session attribution 十一行契约);
  3. 更新两个源 SHA 与验证日期(即第二节所述的 master SHA 与 Go main SHA + "Last verified on" 日期);
  4. Go 的详细设计保留在 PostHog/ai-gateway 仓库(尤其其 docs/product.md),PARITY.md 不复制设计细节,只做决策级对照。

收尾规则同样重要:当一个缺口关闭时,点名明显新具备迁移资格的调用方类别,但不要修改这些调用方——除非用户同时要求迁移工作,那属于 /migrating-llm-gateway-callers 技能的职责边界。这一条把"审计"与"迁移"两个工作流严格解耦,避免审计 PR 悄悄改变线上流量。

八、验证步骤:格式与卫生检查

技能文档给出了审计产物落盘后的三条验证命令,必须全部通过:

pnpm exec oxfmt services/llm-gateway/PARITY.md .agents/skills/auditing-llm-gateway-parity/SKILL.md
pnpm exec markdownlint-cli2 --config .config/.markdownlint-cli2.jsonc services/llm-gateway/PARITY.md .agents/skills/auditing-llm-gateway-parity/SKILL.md
git diff --check
  • oxfmt 对两个 Markdown 文件做格式化,保证表格/列表排版一致;
  • markdownlint-cli2 按仓库统一配置 .config/.markdownlint-cli2.jsonc 做 lint,PARITY.md 的大表格与符号标记(✅/⛔/🔎)都受规则约束;
  • git diff --check 拦截空白错误。

注意验证命令同时覆盖 PARITY.md 与 SKILL.md 本身——技能文档自身也是被 lint 的受控文件,流程与记录同库受检。

九、审计流程全貌与仓库证据索引

把技能文档的五个阶段串起来,一次完整审计的执行序列是:

  1. 锁定基线:记录 origin/master SHA、当前工作树状态、Go 侧 main SHA(经 gh api 获取);
  2. 两侧取证:按第 3、四节的文件清单读实现代码,Python 侧至少覆盖 api/auth/db/required_tables.pyproducts/config.pyrate_limiting/callbacks/posthog/llm/gateway_client.py 的真实调用点;Go 侧覆盖 internal/httpapiinternal/authinternal/principalinternal/ledgerinternal/quotainternal/cataloginternal/routerinternal/dispatchinternal/emitterdocs/product.md
  3. 双向核验:对每条契约同时检查请求与响应行为,把"调用方遥测 ≠ 可信策略"与"unbilled ≠ 自动阻塞"两条规则应用于每条差异;
  4. 三分类落盘:更新 PARITY.md 的场景小节、Parity map、SHA 与日期,不迁移任何调用方;
  5. 验证oxfmt + markdownlint-cli2 + git diff --check 全绿。

关键证据文件速查:

文件 在审计中的角色
.agents/skills/auditing-llm-gateway-parity/SKILL.md 审计流程定义(本文主体)
services/llm-gateway/PARITY.md 审计产物:迁移政策、场景三分类、Parity map、SHA 记录
services/llm-gateway/README.md Python 网关端点/鉴权/计费/路由的起点文档(非证据)
services/llm-gateway/src/llm_gateway/auth/authenticators.py phx_/pha_ 凭据核验实现
services/llm-gateway/src/llm_gateway/db/required_tables.py 最小权限读表白名单与部署前置约束
services/llm-gateway/src/llm_gateway/products/config.py 产品策略、模型白名单、计费桶、服务端凭据门
services/llm-gateway/src/llm_gateway/callbacks/posthog.py 网关自有事件属性与 $ai_billable 归因实现
posthog/llm/gateway_client.py Django 侧真实调用点与 Go 网关构建器

这套技能文档的价值在于把"两个网关到底能不能互相替换"这个容易停留在 README 层面的问题,变成了一条可复现、有版本基线、证据到代码行、结论三分类的工程流程:审计者拿到 SHA 就能重放取证路径,迁移决策者拿到 PARITY.md 就能直接按使用场景查表,而 Python 网关在冻结期的每一次例外变更都必须先通过这条流程自证其必要性。

登录后查看全文
热门项目推荐
相关项目推荐