OpenMed 本地隐私代理(Local Privacy Proxy)实战指南:OpenAI 兼容的端侧 PHI 脱敏边界

OpenMed 本地隐私代理(Local Privacy Proxy)实战指南:OpenAI 兼容的端侧 PHI 脱敏边界 OpenMed 本地隐私代理Local Privacy Proxy实战指南OpenAI 兼容的端侧 PHI 脱敏边界【免费下载链接】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 内置了一个轻量的 OpenAI 兼容 chat-completions 边界服务供已经熟悉消息补全协议的现有应用直接接入它在本机监听请求、调用注入的推理传输层之前先完成文本脱敏并在本地响应中还原占位符——全程不依赖云端患者信息不离开本机。读完本文你将掌握如何用create_app构建本地隐私代理、注入同步/异步传输层、跑通非流式与 SSE 流式补全并理解其请求级脱敏、出站安全扫描与入站占位符还原的完整源码级实现原理。一、定位与适用场景docs/service/privacy-proxy.md明确将这一组件定义为Local privacy proxy本地隐私代理它不是一个独立的模型服务而是一个“边界boundary”。核心职责是接收标准 OpenAI chat-completions 请求在本地对文本内容做 PHI 脱敏替换为确定性的OPENMED_PHI占位符只把脱敏后的 JSON 文档交给注入的推理传输层transport传输层返回后仅在本地响应中还原占位符。这一点决定了它的适用场景应用已经采用 OpenAI 消息协议messages/model/stream希望无痛切换到 OpenMed 的端侧推理管线同时保证进入外部模型或本地模型之前的文本不再携带真实 PHI。实现位于 openmed/service/privacy_proxy/app.py入站还原边界位于 openmed/service/privacy_proxy/inbound.py。二、快速开始两条路由与一个传输层代理同时暴露两个等价端点见 app.pyPOST /v1/chat/completionsPOST /chat/completions兼容无版本前缀的 base URL请求使用标准的 chat-completions 形状{ model: local-fixture, messages: [ {role: user, content: Contact Avery Example at 555-0100.} ] }创建应用的核心是注入一个本地传输层。传输层收到的 payload 是脱敏后的副本其中文本叶子已被替换为确定性的OPENMED_PHI占位符内存中的替换映射placeholder map不会通过 metadata 传给传输层并在请求结束时被丢弃from typing import Any from openmed.service.privacy_proxy import create_app def local_transport(payload: dict[str, Any], **_: Any) - str: # 在这里调用端侧模型。payload 已经是脱敏后的内容。 return payload[messages][-1][content] app create_app(transportlocal_transport)然后以标准方式启动uvicorn my_proxy:app --host 127.0.0.1 --port 8081从源码看create_appapp.py构建 FastAPI 应用并注册两条 POST 路由同时把 proxy 实例挂到fastapi_app.state.privacy_proxy与fastapi_app.state.privacy_proxy_transport上供请求处理器运行时读取。2.1 模块级app对象导入安全但默认拒服务模块底部直接创建了一个无传输层的应用app.pyapp create_app()这个openmed.service.privacy_proxy.app是导入安全的它不会在导入时创建任何网络客户端。但它没有配置传输层因此对补全请求一律返回PHI-free 的503。其背后是_MissingTransport这个 fail-closed 传输层app.py任何调用都会抛出PrivacyProxyConfigurationError。所以在对外提供服务前必须注入一个显式的本地传输层。这一点有测试兜底tests/unit/service/privacy_proxy/test_app.py/health返回{status: ok, transport_configured: false}而POST /v1/chat/completions返回 503且响应正文不回显请求中的输入文本。2.2 健康检查端点GET /healthapp.py只做本地就绪探测不触发模型与传输层调用{status: ok, transport_configured: true}transport_configured反映是否有传输层被注入可用来区分“正常待命”与“未配置即上线”两种状态。三、传输层调用协议同步、异步与流式create_app的transport参数支持多种形态源码在 app.py 中统一处理纯函数transport(payload, **kwargs) - str同步返回文本异步函数返回值是 awaitable 时自动awaitapp.py带属性对象若传输对象具有stream属性且请求是流式则优先调用transport.stream否则若对象具有complete属性且自身不可调用则调用transport.complete流式返回值可以返回同步或异步的可迭代对象元素可以是文本块或 OpenAI 风格的响应块见下文“流式”一节。传输层收到的kwargs由源码按签名兼容性裁剪只传函数实际接受的参数完整元数据为metadata { request_id: prepared.request_id, model: prepared.model, stream: prepared.stream, placeholder_count: len(prepared.placeholder_map), }注意placeholder_count只暴露数量替换映射本身不进入 metadata——这是隐私边界的硬性约束。3.1 请求校验与确定性 ID在脱敏之前_validate_request_payloadapp.py会做 fail-closed 校验messages必须是非空列表每条消息必须包含非空role消息必须包含content或tool_calls/function_callmodel必须是非空字符串缺省为DEFAULT_MODEL openmed-localstream必须是布尔值。请求 ID 的推导app.py有两种来源客户端传入x-request-id请求头须匹配^[A-Za-z0-9_.:-]{1,128}$取sha256(header: 请求头)[:32]否则对 payload 做规范化 JSONsort_keysTrue, ensure_asciiFalse取sha256[:32]。同样的请求会得到同样的 request_id、completion_idchatcmpl-sha256(request_id:model)[:24]与created时间戳digest 前 8 位十六进制转整数——这保证了脱敏 payload 与响应 ID 的确定性便于排查与幂等对齐见测试 test_app.py。四、脱敏管线从文本到占位符PrivacyProxy.prepareapp.py负责一次请求的脱敏它递归遍历 JSON 中的字符串叶子_walk_json_strings对除model、role、type之外的文本调用_redact_value合并生成请求级placeholder_map。占位符格式固定为正则见 app.pyOPENMED_PHI_实体标签_8位HEX_至少6位数字例如OPENMED_PHI_NAME_DEADBEEF_000001。测试中的合成实体即使用该格式test_inbound.py。单段文本的脱敏链路app.py依次为实体提取调用extractor默认openmed.extract_pii传入脱敏模型名、置信度阈值与智能合并开关策略执行policy.enforce(entities)按PrivacyGatewayPolicy拦截被禁实体类别脱敏redact_text(text, entities, request_id...)生成无碰撞占位符与映射实现在 privacy_gateway.py出站安全扫描tripwire对脱敏结果再用独立的确定性扫描器safety_sweep_tripwire底层为openmed.core.safety_sweep.safety_sweepprivacy_gateway.py以置信度阈值 0.0 重新检测若发现残余实体则抛出PrivacyTripwireViolationreason_code outbound_tripwire_detected请求直接以 400 失败——这就是“出站不泄露”的双重保险占位符碰撞检查若同一请求内某个占位符被重复生成报placeholder_collision。4.1 可注入的配置项create_app与PrivacyProxy接受同一组参数app.py参数默认值说明transport无fail-closed注入的推理传输层可为函数或带stream/complete方法的对象extractoropenmed.extract_pii实体提取器接收(text, **kwargs)tripwire_extractorsafety_sweep_tripwire出站残余 PHI 扫描器policyPrivacyGatewayPolicy(min_confidence0.85)隐私策略见下redaction_modelOpenMed/OpenMed-PII-SuperClinical-Small-44M-v1脱敏用模型名PrivacyGatewayPolicyprivacy_gateway.py的关键字段name默认strict、min_confidence默认0.85、detector_confidence_floor默认0.0、disallowed_entity_categories禁用的实体类别集合。五、入站还原边界fail-closed 的占位符恢复传输层返回后代理在本地把占位符还原回真实文本。这一层独立实现于 openmed/service/privacy_proxy/inbound.py核心原则是还原状态请求级、只存内存InboundRestorationState以MappingProxyType包装为只读映射不落盘、不进日志、不进报表还原器InboundPlaceholderRestorer用with语句作为上下文管理器请求结束立即close()释放映射遇到畸形、未知、重复占位符时拒绝还原fail-closed而不是“尽力猜测”。5.1 默认还原策略InboundRestorationPolicy所有上限默认值定义在 inbound.py配置默认值含义max_placeholders256单请求映射中的占位符数量上限max_mapping_bytes1,048,5761 MiB映射 UTF-8 字节上限max_response_bytes4,194,3044 MiB响应原文字节上限max_restored_bytes4,194,3044 MiB还原后内容字节上限max_placeholder_occurrences1,024单响应中占位符出现总次数上限max_response_nodes8,192结构化响应节点数上限max_response_depth32结构化响应最大深度reject_unknown_placeholdersTrue拒绝映射中不存在的占位符reject_duplicate_placeholdersTrue拒绝同一占位符重复出现reject_malformed_placeholdersTrue拒绝格式非法的占位符含OPENMED-PHI碎片策略同时提供reject_unknown/reject_duplicates/reject_malformed三个短别名inbound.py构造时自动映射到完整字段。5.2 还原失败的错误语义inbound.py定义了完整的错误类型族MalformedPlaceholderErrormalformed_placeholder、UnknownPlaceholderErrorunknown_placeholder、DuplicatePlaceholderErrorduplicate_placeholder、DuplicateMappingErrorduplicate_mapping_placeholder、RestorationLimitErrorrestoration_limit_exceeded、UnknownRequestStateErrormissing_request_state、UnsupportedResponseErrorunsupported_response_content、DuplicateResponseKeyErrorduplicate_response_key等。它们在PrivacyProxy.restore_completion/stream_events中被统一转换为PrivacyProxyResponseErrorHTTP 502。5.3 结构化响应还原与并发隔离restore_structured_response会递归还原映射、列表、元组中的所有字符串值与键并对“还原后键冲突”报DuplicateResponseKeyError防止两个不同占位符还原成同一键导致结构坍缩InboundRestorationStore是一个线程安全的有界存储inbound.py默认最多 128 个活动请求、映射总字节 16 MiB永不驱逐活动请求饱和时直接 fail-closed保证请求到映射的一对一隔离。六、流式响应SSE 与跨块占位符还原设置stream: true即可获得标准 server-sent eventsSSE输出。/v1/chat/completions返回media_typetext/event-stream并附带响应头x-privacy-proxy: local与x-request-idapp.py。传输层对流式请求可以返回文本块The contact is 这样的字符串片段OpenAI 风格块形如{choices: [{delta: {content: ...}}]}的映射SSE 原始行以data:开头的字符串会被解析为事件app.py同步或异步可迭代对象以上任意元素的迭代。流式还原的关键机制是_IncrementalRestorerapp.py占位符可能被模型切成两半分多次输出例如先输出OPENMED_PHI_NA再输出ME_DEADBEEF_000001还原器会缓冲以OPENMED_PHI_开头的未闭合片段等闭合后再统一还原每个占位符在整个流中只允许出现一次重复出现即报duplicate_placeholder流在还原该占位符之前中止对应测试 test_app.py输出块被规范化为chat.completion.chunk形状首个块自动补delta.role assistant末尾补finish_reason: stop块最后发送data: [DONE]替换映射本身永远不会作为事件发出——事件流中只有还原后的文本。跨块还原有直接测试证明test_app.py传输层把占位符 token 从中间切开输出最终拼回的流中Avery Example完整出现且流中不含OPENMED_PHI与mapping字样。七、错误模型与安全边界所有代理错误均继承自PrivacyProxyError响应体统一为 PHI-free 结构app.py{ error: { code: privacy_proxy_response_rejected, message: Privacy proxy request failed, details: {reason: duplicate_placeholder} } }异常类HTTPerror_code典型reason_codePrivacyProxyRequestError400privacy_proxy_invalid_requestinvalid_json、messages_required、message_role_required、model_invalid、stream_invalid、request_id_invalidPrivacyProxyRedactionError400privacy_proxy_redaction_failedredaction_failed、placeholder_collision、outbound_tripwire_detectedPrivacyProxyConfigurationError503privacy_proxy_not_configuredmissing_transportPrivacyProxyTransportError502privacy_proxy_transport_failedtransport_not_callable、invalid_transport_responsePrivacyProxyResponseError502privacy_proxy_response_rejectedresponse_rejected、duplicate_placeholder、mangled_placeholder、invalid_stream_chunk值得强调的安全性质均有源码与测试佐证映射不进日志错误消息全部为常量、不含映射值或响应内容inbound.py未配置即失败模块级app不创建网络客户端503 响应不回显请求文本test_app.py出站入站双向校验出站有 tripwire 残余扫描入站有占位符合法性校验reidentify_placeholders拒绝幻觉/被篡改的 tokenprivacy_gateway.py。八、边界与注意事项原文档在结尾给出了明确的使用边界这里原样保留并补充说明这是隐私边界privacy boundary不是合规认证compliance certification也不是临床决策系统。它只保证传输层看到的文本经过脱敏不替代 HIPAA 等合规审计开发阶段务必使用合成数据synthetic fixtures并为部署上下文验证残余风险residual risk脱敏质量取决于注入的extractor与tripwire_extractor默认走openmed.extract_pii与safety_sweep如需要可在create_app时替换为针对业务领域调优的检测器所有限流与预算参数占位符数量、字节上限、深度等都可以通过自定义InboundRestorationPolicy收紧默认值已经偏严格。九、落地清单用create_app(transportlocal_transport)构建应用并确认transport只接收脱敏后的 payload通过GET /health确认transport_configured为true后再对外暴露流式场景下让传输层返回文本块或 OpenAI 风格块验证 SSE 事件流中不含OPENMED_PHI与映射用同一请求重复调用确认脱敏 payload 与id确定便于审计与幂等用合成 PHI 做正反向用例正向验证还原正确反向验证重复/未知/畸形占位符被 400/502 拒绝在部署前评估残余风险并确认代理不会把替换映射写入任何日志、报表或磁盘。本文涉及的实现与测试均可在仓库中进一步研读代理实现、入站还原边界、脱敏与网关原语、代理测试、入站还原测试。【免费下载链接】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),仅供参考