
OpenMed Service API 兼容性门禁基于 OpenAPI 契约的离线破坏性变更检测【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmedOpenMed 将 REST 服务的公开契约固化在 docs/api/openapi.json 中并提供一个免启动服务、免网络请求的兼容性门禁compatibility gate在每次发布前自动对比已签入的契约文档与实时 FastAPI 生成的 schema从而把客户端可见的破坏性变更拦截在发布流程之外。本文以 docs/api/compatibility-gate.md 为主线结合 openmed/service/api_compatibility.py 的完整实现与 tests/unit/service/test_api_compatibility.py 的测试用例讲解该门禁的命令行用法、进程内 API、检查维度、稳定错误类别与 PHI 安全设计帮助你直接把它接入自己的 CI 流水线。为什么需要兼容性门禁OpenMed 的服务以 REST 为对外主要入口同时暴露 Python PII API 与 MCP 工具。任何一个移除路由给请求体新增必填字段删除某个错误类别的改动都可能让已经上线的客户端在升级后立即失败。而人工审查 OpenAPI 文档差异既容易遗漏嵌套字段也无法保证与真实服务一致。兼容性门禁解决的是发布安全性问题它用机器可读、确定性输出的方式回答新的服务实现是否仍然满足已签入的契约。注意它的定位有明确边界——它是一次发布安全检查release-safety check不是合规认证也不是临床决策保障不能替代 docs/api/errors.md 所描述的错误语义评审。核心思路把两份契约降到稳定子集再比较门禁并不对两份 OpenAPI 文档做逐字 diff而是先把它们规约reduce为客户端真正依赖的稳定信息。这在源码中体现为ServiceContract、ContractOperation、RequiredField三个冻结数据类见 openmed/service/api_compatibility.pyContractOperation一个(method, route)操作、其请求体是否必填、请求 schema 引用、必填字段列表RequiredField必填字段的relative_path如options/mode与schema_pathJSON Pointer 定位如#/components/schemas/AnalyzeRequest/properties/options/properties/modeServiceContract全部操作 稳定错误类别集合。规约过程刻意忽略描述文本、示例、默认值、响应载荷等值信息。模块 docstring 明确写道这种规约保证报告不会回显请求内容见 openmed/service/api_compatibility.py这一点对以去标识化为核心的 OpenMed 尤其关键。契约从哪来基线beforeload_contract()直接读取仓库根目录下签入的 docs/api/openapi.jsonOpenAPI 3.1.0当前包含 19 个路径覆盖/analyze、/pii/deidentify、/pii/extract、/ground、/health、/readyz等不导入任何服务代码实时afterbuild_live_contract()在进程内调用 FastAPI app 的app.openapi()方法获取 schema同样不监听端口、不发起任何网络请求见 openmed/service/api_compatibility.py。若未显式传入 app则通过create_app()构造一个。默认契约路径由DEFAULT_CONTRACT_PATH REPOSITORY_ROOT / docs / api / openapi.json定义可通过--contract覆盖。命令行快速上手门禁以模块形式提供了 CLI 入口即__main__分支调用main()见 openmed/service/api_compatibility.py# 输出人类可读的检查结论 python -m openmed.service.api_compatibility # 输出确定性的机器可读报告CI 首选 python -m openmed.service.api_compatibility --json # 指定自定义契约文件 python -m openmed.service.api_compatibility --contract docs/api/openapi.json --jsonCLI 参数与退出码约定参数作用默认值--contract PATH指定要对比的已签入 OpenAPI 契约docs/api/openapi.json--json输出机器可读 JSON 报告关闭输出人类可读文本退出码含义0契约兼容Service API contract: compatible1发现破坏性变更打印每条 breaking issue 的describe()2契约本身无效如 JSON 无法解析、结构不合法输出invalid_contract--json模式的报告结构由CompatibilityReport.to_dict()定义见 openmed/service/api_compatibility.py固定包含schema_version、summary、added、breaking、compatible五个部分且to_json()使用sort_keysTrue保证字节级稳定便于作为 CI 工件或 diff 依据{ schema_version: 1, summary: { before_operations: 24, after_operations: 25, added: 1, breaking: 0 }, added: [{change: operation_added, route: POST /models/unload}], breaking: [], compatible: [] }门禁检查什么三个稳定维度门禁只关注客户端可见的三类契约信息原文明确列出的检查范围路由与 HTTP 方法组合例如POST /analyze、GET /health是否存在。方法集合限定为delete/get/head/options/patch/post/put/trace非标准字段会被跳过必填请求体字段包括嵌套对象字段、$ref引用展开、allOf组合以及数组items内的必填字段稳定的机器可读错误类别即STABLE_ERROR_CATEGORIES全集见下文。破坏性 vs 非破坏性分类原文给出了明确的判定规则与源码中compare_contracts()的breaking/added/compatible三分类一一对应见 openmed/service/api_compatibility.py变更类型分类说明移除操作routemethodbreakingoperation_removed请求体由可选变必填breakingrequest_body_required移除请求 schemabreakingrequest_schema_removed新增必填字段含嵌套breakingrequired_field_added移除错误类别breakingerror_category_removed新增路由/操作added不失败operation_added新增可选字段不产生 issue—移除必填字段compatible不失败required_field_removed新增错误类别added不失败error_category_added关键点新增必填字段是破坏性的而移除必填字段反而只是兼容变更——因为旧客户端不会发送新字段但新服务一旦强制要求旧客户端没发过的字段就会直接 422。同理新增路由不破坏旧客户端删除路由则必然破坏。嵌套字段与组合 schema 的处理细节必填字段的提取在_required_fields()中递归完成见 openmed/service/api_compatibility.py有几个容易踩坑的细节值得关注$ref展开递归解析#/...JSON Pointer用seen_refs防止循环引用遇到循环即停止该分支anyOf/oneOf变体只有出现在所有变体中的必填字段才被计入避免把某一分支才需要的字段误判为无条件必填测试test_union_variant_field_is_not_treated_as_unconditionally_required专门验证这一点数组items沿items递归relative_path中以[]表示数组层级如patients[]/name必填字段判定只看 schema 的required关键字与字段是否存在于properties无关且字段以排序后的顺序输出保证确定性。稳定的错误类别清单门禁比较的错误类别来自STABLE_ERROR_CATEGORIES见 openmed/service/api_compatibility.py它是两部分类别的排序并集服务端 legacy 稳定类别源码中的_LEGACY_STABLE_ERROR_CATEGORIES公共 Python API 注册表openmed.core.errors.ERROR_CODES的全部 code 值见 openmed/core/errors.py即openmed_error、input_error、configuration_error、capability_error、missing_extra、model_load_error、policy_error、budget_exceeded、internal_error、inference_error。合并后的完整稳定类别如下约 46 个按字母序auth_rate_limited、authentication_required、backpressure、bad_request、budget_exceeded、capability_error、circuit_breaker_open、configuration_error、forbidden、grounding_invalid_request、inbound_duplicate_mapping、inbound_duplicate_placeholder、inbound_duplicate_response_key、inbound_malformed_placeholder、inbound_request_scope_error、inbound_restoration_error、inbound_restoration_limit、inbound_unknown_placeholder、inbound_unsupported_response、inference_error、input_error、internal_error、invalid_credentials、missing_extra、model_load_error、not_ready、offline_snapshot_unavailable、openmed_error、policy_error、privacy_gateway_blocked、privacy_gateway_error、privacy_gateway_not_configured、privacy_gateway_reidentification_error、privacy_gateway_transport_error、privacy_proxy_error、privacy_proxy_invalid_request、privacy_proxy_not_configured、privacy_proxy_redaction_failed、privacy_proxy_response_rejected、privacy_proxy_transport_failed、rate_limited、restricted_terminology_unconfigured、service_busy、snapshot_invalid、timeout、validation_error。这些类别与 docs/api/errors.md 中结构化公共错误的 taxonomy 及 HTTP/MCP 映射保持一致例如input_error对应 HTTP 400 与 MCP codeinput_errorcapability_error/missing_extra/model_load_error对应 503internal_error/inference_error对应 500FastAPI 请求 schema 校验失败仍使用 HTTP 422 与validation_error。类别名必须匹配^[a-z][a-z0-9_]{0,63}$见_CATEGORY_PATTERN任何大写字母、连字符或超长类别都会被APIContractError拒绝且拒绝信息不会回显被拒绝的值本身测试test_invalid_error_categories_are_rejected_without_echoing_values验证了这一点。错误类别如何被发现build_live_contract()优先读取 app 对象上的service_error_categories/error_categories属性若不存在则调用discover_service_error_categories()见 openmed/service/api_compatibility.py它用 Pythonast模块扫描openmed/service下所有源码文件提取error_code ...赋值、_error_response(..., ...)调用与AuthError(code...)关键字参数中的字符串字面量再显式补上bad_request/internal_errorHTTP 处理器用条件表达式映射、不直接出现在字面量中以及ERROR_CODES.values()。这样即使新增了错误类别而忘记更新文档门禁也能从源码自动发现。进程内 API在测试与发布脚本中集成除了 CLI门禁暴露了三个 Python 入口见 openmed/service/api_compatibility.pyfrom openmed.service.api_compatibility import ( check_api_compatibility, assert_api_compatibility, CompatibilityReport, ) # 1. 返回报告不抛异常 report: CompatibilityReport check_api_compatibility() print(report.is_compatible, report.breaking_count) # 2. 失败关闭存在破坏性变更时抛 APICompatibilityError report assert_api_compatibility() # 3. 用自定义 FastAPI app 做进程内对比无需启动服务 report check_api_compatibility(appmy_app)CompatibilityReport提供以下常用成员见 openmed/service/api_compatibility.pyis_compatiblenot self.breaking布尔结论breaking_count破坏性变更数量failing_issues()/breaking破坏性 issue 元组按(change, route, schema_path, error_category)确定性排序added/compatible非破坏性 issueto_dict()/to_json()PHI-free 的机器可读报告。assert_api_compatibility()在发现破坏性变更时抛出APICompatibilityError其report属性携带完整报告异常消息由各 breaking issue 的describe()拼接格式如required_field_added: POST /analyze at #/components/schemas/AnalyzeRequest/properties/options/properties/mode不会包含任何请求示例或字段值。PHI 安全报告只含位置不含值对 OpenMed 而言兼容性报告本身也是一条需要防泄漏的数据。实现从三个层面保证 PHI 安全规约即过滤CompatibilityIssue.to_dict()只输出change、route、schema_path、error_category四个键见 openmed/service/api_compatibility.pydescribe()也只拼接这些定位信息契约加载阶段load_contract()读取的 OpenAPI 文档若无法解析或结构非法抛出APIContractError消息是固定的模板文本如 The API contract could not be read不会拼接原始内容测试固化测试test_matching_contract_is_deterministic_and_compatible与test_removed_route_required_field_and_error_category_are_breaking都断言SYNTHETIC_INPUT_SENTINEL不会出现在to_dict()、异常消息或to_json()中——即契约文档里的示例值永远不会泄漏进报告。这一点与 docs/api/errors.md 的 PHI-safe 诊断策略一脉相承错误消息从不包含源临床文本、检测到的标识符表面、可逆映射、凭据或密钥材料。测试与 CI 落地建议仓库为门禁提供了完整的单元测试tests/unit/service/test_api_compatibility.py覆盖了以下关键行为可直接作为你接入 CI 的验收基准测试用例验证点test_matching_contract_is_deterministic_and_compatible相同契约两次比较结果字节级一致test_removed_route_required_field_and_error_category_are_breaking三类破坏性变更同时被检出且报告不含示例值test_additive_route_optional_field_and_error_category_are_allowed新增路由/错误类别不失败test_nested_required_field_uses_a_schema_path嵌套必填字段给出完整 JSON-schema 路径test_union_variant_field_is_not_treated_as_unconditionally_requiredanyOf变体字段不被误判为必填test_check_gate_reads_baseline_and_uses_an_offline_app进程内对比无需网络传入合成 app 即可test_invalid_error_categories_are_rejected_without_echoing_values非法类别被拒且不回显test_checked_in_contract_matches_live_service_surface签入契约与真实服务表面一致当前通过test_live_error_categories_are_discovered_from_local_service_sourceAST 发现的类别等于稳定全集在 CI 中的典型接法在服务代码合并后运行python -m openmed.service.api_compatibility --json api-compat-report.json并以退出码判断门禁是否通过将api-compat-report.json作为构建工件归档即可保留每次发布的契约差异轨迹。注意该门禁是本地、确定性的不依赖任何外部服务因此可以在离线构建环境或发布流水线的早期阶段执行。扩展性与维护要点从源码结构可以推断出门禁刻意设计为可演进的SCHEMA_VERSION 1是报告格式的版本号未来报告结构变化时通过它向后兼容解析方OpenAPI 文档可通过顶层x-openmed-error-categories扩展ERROR_CATEGORIES_EXTENSION见 openmed/service/api_compatibility.py显式声明错误类别集合normalize_contract()优先读取该扩展其次回退到默认稳定全集——这为契约作者提供了显式声明入口compare_api_contracts、run_compatibility_gate是compare_contracts、check_api_compatibility的兼容别名说明门禁本身也遵守不破坏已有调用方的承诺。结语OpenMed 的兼容性门禁把发布是否破坏客户端这个模糊问题变成了一个确定性的、可脚本化的、PHI 安全的机械判定它不启动服务、不发网络请求却能在每次发布前覆盖路由、必填字段与错误类别三个客户端最敏感的契约维度。配合 docs/api/openapi.json 作为单一事实来源、docs/api/errors.md 作为错误语义文档以及仓库内完整的单元测试你可以在自己的 CI 流水线中以最轻量方式复用它。需要再次强调的是它解决的是发布安全检查问题任何错误类别或字段语义的变更仍应结合 docs/api/errors.md 的 taxonomy 做人工评审切勿将门禁视为合规或临床安全的替代品。【免费下载链接】openmedLocal-first healthcare AI: clinical NER HIPAA PII de-identification that runs 100% on-device. 2,200 medical models, 21 languages, Apple MLX Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考