AstrLink Guard is a lightweight model for detecting personal information and credentials in Chinese, English and mixed Chinese–English text, including source code, configuration files, logs and coding-assistant requests. It runs fully locally on CPU with ONNX Runtime and returns entity spans with labels and scores, so the calling application decides what to mask.
It is the privacy detection model used by AstrLink.
This repository contains version 0.1.1 in two formats: int8/ (dynamic INT8, recommended) and fp32/ (FP32, higher accuracy).
中文说明
What's new in 0.1.1
- Fewer credentials left unmasked. At the recommended minimum score of 0.8, credentials in coding-assistant requests that are missed with no part masked drop from 434 to 233 with INT8 and from 352 to 183 with FP32 (of 2,182). AWS access key IDs (
AKIA…) drop from 80 to 7 with INT8 and from 78 to 5 with FP32 (of 91).
- Fewer false positives in coding-assistant traffic. On requests that contain no personal data or credentials, the share of requests with at least one flagged span drops from 57.2% to 11.0% with INT8 and from 66.5% to 17.3% with FP32 (at the recommended minimum score of 0.8). Hashes, digests, IDs and base64 blobs in tool output are flagged much less often.
- Better coverage of keys inside URLs. API keys and tokens passed in URL query strings, paths and connection strings are now detected separately from the URL: key coverage goes from 64.4% to 98.2% with INT8 and from 68.0% to 98.7% with FP32, and keys swallowed into a URL span drop from 68 (INT8) and 63 (FP32) to 2.
- Higher accuracy on coding-assistant requests: macro F1 over the 10 types goes from 70.23 to 88.35 with INT8 and from 70.42 to 88.68 with FP32, with the largest gains on
account and common_secret.
- Recommended minimum span score: 0.8. Per-type scores are calibrated for it, and AstrLink uses it by default. Integrations that apply a lower minimum score should raise it to 0.8.
- INT8 is now the recommended format. FP32 stays available in
fp32/ for the highest accuracy.
- Unchanged from 0.1.0: architecture, tokenizer, the 10 labels, the input contract, the decoding rules and the repository layout. Existing integrations can switch models without code changes.
Model details
|
|
| Version |
0.1.1 (previous release: 0.1.0) |
| Architecture |
BertForTokenClassification, 12 layers, hidden size 384 |
| Parameters |
36.9M |
| Base model |
microsoft/Multilingual-MiniLM-L12-H384 |
| Tokenizer |
XLM-R SentencePiece Unigram, 40,003 tokens |
| Languages |
Chinese, English |
| Window |
512 tokens (128-token overlap for long inputs) |
| Maximum request |
131,072 tokens |
| Formats |
ONNX dynamic INT8 (recommended), ONNX FP32; weights in an external data file |
| License |
Apache-2.0 |
Formats
| Folder |
Format |
Model size |
general F1 |
code F1 |
coding-assistant macro F1 |
int8/ (recommended) |
Dynamic INT8 |
36 MiB |
96.79 |
91.70 |
88.35 |
fp32/ |
FP32 |
141 MiB |
97.18 |
92.16 |
88.68 |
Use int8/ by default. It is about a quarter of the size of FP32 and usually faster on CPU, and it is the format AstrLink recommends by default.
Use fp32/ when accuracy matters most, especially for code and API keys. Compared with FP32, INT8 is −0.39 F1 points on general text, −0.46 on code (−0.59 on common_secret in code) and −0.32 macro F1 points on coding-assistant requests. At the recommended minimum score of 0.8, INT8 reaches a coding-assistant macro F1 of 88.63 against 89.84 for FP32, and leaves 233 of 2,182 credentials in coding-assistant requests completely unmasked against 183 for FP32 (AWS access key IDs: 7 against 5, of 91). In return, INT8 flags fewer harmless values: 11.0% of entity-free coding-assistant requests get a false positive, against 17.3% for FP32.
Files
| Path |
Description |
int8/model_int8.onnx |
INT8 ONNX graph |
int8/model_int8.onnx_data |
INT8 weights (external data, must stay next to model_int8.onnx) |
fp32/model.onnx |
FP32 ONNX graph |
fp32/model.onnx_data |
FP32 weights (external data, must stay next to model.onnx) |
config.json |
Model config, label map and decoding contract (one in each folder) |
tokenizer.json, tokenizer_config.json |
Tokenizer (one in each folder) |
labels.json, label_mapping.json |
BIO tags and entity type mapping (one in each folder) |
astrlink-model.json |
AstrLink import descriptor (one in each folder) |
CHECKSUMS.json |
sha256 and size of the model files in the folder |
licenses/ |
License texts, notices and data sources |
SHA256SUMS |
sha256 of every file in this repository |
Each of int8/ and fp32/ is a complete model folder. The config.json at the repository root is the same file as in int8/ and fp32/.
| File |
Size (bytes) |
sha256 |
int8/model_int8.onnx |
324,054 |
e02ba712558a3c08fe2d0de8badbbf741d80bf323bbc4166e8f92f985d97fef3 |
int8/model_int8.onnx_data |
37,046,272 |
2a9c01ef8a1d6b462abd8ecac1e5fcb885200206cc51ea91967c78e691afa200 |
fp32/model.onnx |
219,740 |
bb3e21f128c2bf1276f0fc1d1a7b5dde0ff9283a94a74e6f045f65dd4c8d1407 |
fp32/model.onnx_data |
147,443,712 |
93d3bf7c0f5d3387553ceec6e8c25dc12a8927e15c5ab7b101e19b7e433a616c |
tokenizer.json (both folders) |
1,502,881 |
0cf3b235ef125658015a6c8026a5fe6f28088bf825101a990ec18a10200feef4 |
config.json (root and both folders) |
1,804 |
2c20003590d642e273c03c4c7c82d9849fa75f64bf29a71ec0a0c9a20ba4a150 |
Entity types
BIO tagging, 21 tags.
| Label |
Description |
private_person |
Person name |
phone |
Phone number (mobile, or landline with area code) |
email |
Email address |
private_address |
Physical address (home, shipping, office) |
private_date |
Date, time of day or timestamp |
account |
Personal account or service number: user name, QQ / WeChat ID, member or employee number, non-bank stored-value or campus card number |
payment_card |
Bank or credit card number |
ip_address |
IP address (without port) |
url |
URL |
common_secret |
Password, PIN, API key, access token, cookie key=value, private key, public key |
Not labeled as entities: order, ticket, tracking and transaction numbers; amounts, counts, durations and version numbers; hashes, checksums and request IDs; key fingerprints and signatures; OAuth client IDs; short internal numeric IDs; partially masked values (for example only the last 4 characters visible); placeholders.
Quick start
pip install onnxruntime tokenizers numpy huggingface_hub
import json
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
from tokenizers import Tokenizer
d = snapshot_download("QuantumNous/astrlink-guard", allow_patterns=["config.json", "int8/*"]) + "/int8"
onnx_file = "model_int8.onnx" # for the FP32 variant: allow_patterns=["config.json", "fp32/*"], "/fp32", "model.onnx"
tok = Tokenizer.from_file(f"{d}/tokenizer.json")
id2label = json.load(open(f"{d}/labels.json"))["id2label"]
sess = ort.InferenceSession(f"{d}/{onnx_file}", providers=["CPUExecutionProvider"])
def detect(text):
enc = tok.encode(text) # one window: at most 512 tokens including <s> and </s>
ids = np.array([enc.ids], dtype=np.int64)
feed = {"input_ids": ids, "attention_mask": np.ones_like(ids), "token_type_ids": np.zeros_like(ids)}
logits = sess.run(["logits"], feed)[0][0]
probs = np.exp(logits - logits.max(-1, keepdims=True))
probs /= probs.sum(-1, keepdims=True)
spans, cur = [], None
for label_id, p, (start, end) in zip(probs.argmax(-1), probs.max(-1), enc.offsets):
if start == end: # special tokens
continue
tag, _, kind = id2label[str(label_id)].partition("-")
if tag == "O":
cur = None
elif tag == "B" or cur is None or cur["label"] != kind:
cur = {"label": kind, "start": start, "end": end, "score": float(p)}
spans.append(cur)
else:
cur["end"] = end
cur["score"] = min(cur["score"], float(p))
for s in spans:
s["text"] = text[s["start"]:s["end"]]
return spans
print(detect("请联系张伟,电话 13812345678,邮箱 [email protected],收货地址:北京市海淀区中关村大街 27 号。"))
To pin this release, pass revision="v0.1.1" to snapshot_download.
Example results (INT8; all values are made up):
请联系张伟,电话 13812345678,邮箱 [email protected],收货地址:北京市海淀区中关村大街 27 号。
private_person 张伟 0.9968
phone 13812345678 0.9966
email [email protected] 0.9974
private_address 北京市海淀区中关村大街 27 号 0.9779
Contact Jane Doe at [email protected] or +1 415-555-0132. Server 10.0.3.17, api_key = "q7Vt2LmX9pRk4sWz8bNc3yHd6fJg1aE5"
private_person Jane Doe 0.9968
email [email protected] 0.9966
phone +1 415-555-0132 0.9966
ip_address 10.0.3.17 0.9795
common_secret q7Vt2LmX9pRk4sWz8bNc3yHd6fJg1aE5 0.9903
订单号 202409301234567890,金额 128.00 元,已于 2026-09-30 14:05 发货。
private_date 2026-09-30 14:05 0.9828
The FP32 variant returns the same spans for these examples, with scores within 0.02 of INT8.
Offsets are Unicode character positions in the input string.
Long inputs
The example above handles one 512-token window. For longer text, run 512-token windows with a 128-token overlap and merge the spans. AstrLink accepts up to 131,072 tokens per request and rejects longer input explicitly instead of truncating it.
Decoding and scores
The decoding rules are recorded in config.json: BIO decoding constrained by character offsets (bio-offset-consistency-v1), plus boundary cleanup for IP addresses and URLs (existing-ip-and-dynamic-url-v1). Plain argmax decoding, as in the example, may differ slightly.
Every span carries a score. The recommended minimum score is 0.8: drop spans that score below it. This release is calibrated for that setting, and AstrLink applies it by default. A higher minimum score trades some recall for fewer false positives; a lower one adds false positives.
Evaluation
Evaluated on in-house benchmarks. P / R / F1 are entity-level exact-match percentages; 95% confidence intervals come from document-level bootstrap. Unless noted, no minimum score is applied.
General text (2,200 documents: 840 English, 840 Chinese, 520 mixed)
|
Entities |
FP32 P |
FP32 R |
FP32 F1 |
INT8 F1 |
| All |
4,336 |
97.38 |
96.98 |
97.18 [96.62, 97.74] |
96.79 |
| English |
1,730 |
95.76 |
95.26 |
95.51 |
95.06 |
| Chinese |
1,694 |
97.74 |
97.23 |
97.48 |
96.92 |
| Mixed |
912 |
99.78 |
99.78 |
99.78 |
99.84 |
| Entity type |
Entities |
FP32 F1 |
INT8 F1 |
| url |
360 |
100.00 |
100.00 |
| email |
407 |
99.51 |
98.65 |
| ip_address |
315 |
99.68 |
99.68 |
| payment_card |
319 |
99.21 |
99.21 |
| phone |
446 |
98.11 |
97.78 |
| private_person |
507 |
96.97 |
97.06 |
| common_secret |
352 |
97.87 |
97.30 |
| private_address |
365 |
97.95 |
97.68 |
| private_date |
751 |
94.43 |
93.81 |
| account |
514 |
92.81 |
91.86 |
Macro F1 over the 10 types: FP32 97.66, INT8 97.30. False positives on documents with no entities: FP32 1 of 662 (0.2%), INT8 1 of 662 (0.2%).
Code, configs and logs (709 documents)
|
Entities |
FP32 P |
FP32 R |
FP32 F1 |
INT8 F1 |
| All |
1,499 |
91.40 |
92.93 |
92.16 [90.13, 93.86] |
91.70 |
| English |
763 |
89.95 |
90.30 |
90.12 |
90.00 |
FP32 by type: url 96.67, common_secret 91.41, account 89.86, ip_address 100.00, private_date 100.00, private_person 94.74. False positives on documents with no entities: FP32 11 of 261 (4.2%), INT8 9 of 261 (3.4%).
Coding-assistant requests (1,200 requests, 22,667 text segments)
Fictional API requests from coding assistants (Anthropic Messages, OpenAI Responses and Chat Completions formats). As in AstrLink, every string in the request JSON is a separate text segment. 173 requests contain no entity at all; a request counts as a false positive if any of its segments gets at least one span.
|
FP32 |
INT8 |
| Macro F1, 10 types |
88.68 [87.75, 89.53] |
88.35 |
| Macro F1, weakest language (English) |
87.45 |
87.08 |
| Macro F1, minimum score 0.8 |
89.84 |
88.63 |
account F1 |
66.67 |
67.72 |
common_secret F1 |
84.75 |
84.61 |
| Entity-free requests with a false positive |
42.2% |
32.9% |
| Same, minimum score 0.8 |
17.3% |
11.0% |
| Credentials missed with no part masked, minimum score 0.8 |
183 / 2,182 |
233 / 2,182 |
| AWS access key IDs missed with no part masked, minimum score 0.8 |
5 / 91 |
7 / 91 |
A credential counts as missed with no part masked when no kept span of any type overlaps it, so its full value would be sent in clear text.
Keys inside URLs (300 documents, 225 keys, 248 URLs)
Keys and tokens placed in URL query strings, paths, fragments and connection strings, in Chinese, English and mixed text.
|
FP32 |
INT8 |
Keys detected as common_secret |
222 / 225 (98.7%) |
221 / 225 (98.2%) |
| Same, minimum score 0.8 |
221 / 225 |
220 / 225 |
| Keys swallowed into a URL span |
2 |
2 |
| URLs with exact boundaries |
240 / 248 |
241 / 248 |
Compared with 0.1.0
Same benchmarks, same scoring, same format on both sides.
|
0.1.0 INT8 |
0.1.1 INT8 |
0.1.0 FP32 |
0.1.1 FP32 |
| Coding-assistant macro F1 |
70.23 |
88.35 |
70.42 |
88.68 |
| Entity-free requests with a false positive (min score 0.8) |
57.2% |
11.0% |
66.5% |
17.3% |
| Credentials missed with no part masked (min score 0.8, of 2,182) |
434 |
233 |
352 |
183 |
| AWS access key IDs missed with no part masked (min score 0.8, of 91) |
80 |
7 |
78 |
5 |
| Keys inside URLs detected |
64.4% |
98.2% |
68.0% |
98.7% |
| Keys swallowed into a URL span |
68 |
2 |
63 |
2 |
| URLs with exact boundaries |
148 / 248 |
241 / 248 |
157 / 248 |
240 / 248 |
| General text F1 |
96.48 |
96.79 |
96.49 |
97.18 |
| Code F1 |
87.91 |
91.70 |
88.97 |
92.16 |
Limitations
- Like any detector, the model can miss entities or flag harmless values. Use it as one layer of privacy protection, not as a guarantee.
- INT8 trades some accuracy for size: in coding-assistant requests it leaves more credentials completely unmasked than FP32 (233 against 183 of 2,182 at minimum score 0.8). Use
fp32/ where a missed key costs more than a larger model.
- Code and coding-assistant traffic are harder than prose. With INT8, about 3.4% of entity-free code documents and 11.0% of entity-free coding-assistant requests still get at least one false positive (FP32: 4.2% and 17.3%); long tool outputs full of IDs, digests and partially masked keys are the main source.
account is the weakest entity type in code and coding-assistant requests.
- Passwords, and keys with no field name or vendor prefix nearby (such as dash-separated keys), are the most likely to be missed.
- Card numbers from issuers not seen in training, such as transit, membership or badge numbers, may be labeled
payment_card instead of account.
- Configuration values whose key name ends in
key may be labeled common_secret even when they are not secrets.
- Government ID numbers (such as national ID cards) have no dedicated entity type.
- Quality is measured on Chinese and English only.
Training data
Public datasets and repositories used in training:
The model was also trained on synthetic, fictional examples written for this project, including code, configuration files, logs and coding-assistant requests. The full list with revisions and license texts is in licenses/. No training data is included in this repository.
License
The model is released under the Apache License 2.0. The base model, microsoft/Multilingual-MiniLM-L12-H384, is MIT-licensed.
Attribution:
- Contains data derived from NVIDIA Nemotron-PII, licensed under CC BY 4.0. The data was modified.
- Contains data derived from google/eng-practices, licensed under CC BY 3.0. The data was modified.
- Contains data derived from Django (BSD-3-Clause). This model is not endorsed by the Django project or its contributors.
Full license texts and notices for the base model, datasets and source repositories are in licenses/. SHA256SUMS lists the sha256 of every file in this repository.
中文说明
AstrLink Guard 是一个轻量级的隐私与凭证检测模型,支持中文、英文和中英混排文本,也覆盖源代码、配置文件、日志和编程助手请求。模型用 ONNX Runtime 在本地 CPU 上运行,输出实体片段的位置、类别和分数,是否遮蔽由调用方决定。它是 AstrLink 使用的隐私检测模型。
本仓库提供 0.1.1 版的两种格式:int8/(动态 INT8,推荐)和 fp32/(FP32,精度更高)。
0.1.1 的变化
- 完全没遮住的凭证更少:按建议最低分数 0.8,编程助手请求里一点都没被遮住的凭证,INT8 从 434 个降到 233 个,FP32 从 352 个降到 183 个(共 2,182 个);AWS 访问密钥 ID(
AKIA…)INT8 从 80 个降到 7 个,FP32 从 78 个降到 5 个(共 91 个)。
- 编程助手请求的误报更少:不含隐私和凭证的请求里,出现误报的比例 INT8 从 57.2% 降到 11.0%,FP32 从 66.5% 降到 17.3%(建议最低分数 0.8)。工具输出里的哈希、摘要、ID、base64 被误判的情况明显减少。
- URL 里的密钥识别更全:放在 URL 查询参数、路径和连接串里的 API key、token 会和 URL 分开识别。覆盖率 INT8 从 64.4% 提高到 98.2%,FP32 从 68.0% 提高到 98.7%;被并进 URL 片段的密钥 INT8 从 68 个、FP32 从 63 个降到 2 个。
- 编程助手请求整体更准:十类 macro F1 INT8 从 70.23 提高到 88.35,FP32 从 70.42 提高到 88.68,账号和凭证两类提升最多。
- 建议最低分数 0.8:各类分数按这个值校准,AstrLink 默认也用它。接入方如果用的最低分数更低,请调到 0.8。
- 推荐格式改为 INT8:需要最高精度时仍可用
fp32/。
- 与 0.1.0 相同:模型结构、词表、10 类标签、输入契约、解码规则和仓库目录结构。现有接入换模型不用改代码。
两种格式
| 目录 |
格式 |
模型大小 |
通用文本 F1 |
代码 F1 |
编程助手 macro F1 |
int8/(推荐) |
动态 INT8 |
36 MiB |
96.79 |
91.70 |
88.35 |
fp32/ |
FP32 |
141 MiB |
97.18 |
92.16 |
88.68 |
- 默认用
int8/:体积约为 FP32 的四分之一,CPU 上通常更快,也是 AstrLink 默认推荐的格式。
- 最看重精度时用
fp32/,尤其是代码和 API key:与 FP32 相比,INT8 通用文本 F1 低 0.39 点,代码低 0.46 点(代码中的 common_secret 低 0.59 点),编程助手 macro F1 低 0.32 点。按建议最低分数 0.8,编程助手 macro F1 INT8 为 88.63、FP32 为 89.84;编程助手请求里完全没遮住的凭证 INT8 为 233 个、FP32 为 183 个(共 2,182 个),AWS 访问密钥 ID 为 7 个对 5 个(共 91 个)。
- 另一方面,INT8 误报更少:不含实体的编程助手请求里,出现误报的比例 INT8 为 11.0%,FP32 为 17.3%。
int8/、fp32/ 各是一个完整的模型目录。仓库根目录的 config.json 与 int8/、fp32/ 中的 config.json 相同。
实体类别
人名、电话、邮箱、物理地址、日期时间、个人账号、银行卡、IP、URL、凭证(密码、PIN、API key、token、Cookie、私钥、公钥),BIO 共 21 个标签。
订单号、快递号、交易号、金额、版本号、哈希、请求 ID、OAuth client_id、部分脱敏的值、占位符等不属于实体。
用法
见上面的 Quick start:示例默认下载并加载 int8/;换成 FP32 时下载 fp32/,加载 model.onnx。要固定在本版,给 snapshot_download 加 revision="v0.1.1"。示例只处理一个 512 token 的窗口;长文本按 512 token 窗口、128 token 重叠切分,再合并结果。单次请求最长 131,072 token,超过时明确拒绝,不截断。
每个片段都带分数。建议最低分数 0.8:丢弃分数低于它的片段。本版按这个设置校准,AstrLink 默认也用这个值;调高最低分数会少一些误报,也会多一些漏检,调低则误报增多。
效果
| 评测集 |
INT8 |
FP32 |
| 通用文本 F1(2,200 篇,中 / 英 / 混排) |
96.79 |
97.18 |
| 代码、配置、日志 F1(709 篇) |
91.70 |
92.16 |
| 编程助手请求 macro F1(1,200 个请求) |
88.35 |
88.68 |
| 无实体请求的误报比例(最低分数 0.8) |
11.0% |
17.3% |
| 完全没遮住的凭证(最低分数 0.8,共 2,182 个) |
233 |
183 |
| 完全没遮住的 AWS 访问密钥 ID(最低分数 0.8,共 91 个) |
7 |
5 |
| URL 中密钥覆盖率(225 个密钥) |
98.2% |
98.7% |
通用文本上:中文 F1 INT8 96.92、FP32 97.48,英文 95.06、95.51,混排 99.84、99.78;不含实体的文档误报率两种格式均为 0.2%。与 0.1.0 在同样评测方式下的对比见上面 “Compared with 0.1.0”。
局限
- 检测模型都会有漏检和误报,请把它当作隐私保护的一层,不要当作保证。
- INT8 用一部分精度换体积:编程助手请求里完全没遮住的凭证比 FP32 多(最低分数 0.8 时 233 个对 183 个,共 2,182 个)。漏掉一个密钥的代价高于模型体积时,请用
fp32/。
- 代码和编程助手请求比普通文本难:用 INT8 时,不含实体的代码文档约 3.4%、不含实体的编程助手请求约 11.0% 仍会出现误报(FP32 为 4.2% 和 17.3%),主要来自满是 ID、摘要、部分脱敏 key 的长工具输出。
- 代码和编程助手请求里,账号识别最弱。
- 口令,以及附近既没有字段名、也没有厂商前缀的密钥(例如用连字符分段的密钥),最容易漏。
- 训练中没见过的发卡方(交通卡、会员卡、工牌等)的卡号,可能被判成银行卡。
- 键名以 key 结尾的配置值,即使不是凭证,也可能被判成凭证。
- 身份证等政府证件号没有专门的类别。
许可
模型采用 Apache-2.0;底座模型 MiniLM 为 MIT。训练用到的公开数据包括 NVIDIA Nemotron-PII(CC BY 4.0)、google/eng-practices(CC BY 3.0)、Django(BSD-3-Clause)等,另有为本项目编写的虚构合成样本。署名见上面 License 一节,许可原文和数据来源见 licenses/。