Text Classification
Transformers
Safetensors
deberta-v2
agents
agent-safety
tool-use
prompt-injection
deberta-v3
text-embeddings-inference
Instructions to use kontext-security/Merlin with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use kontext-security/Merlin with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="kontext-security/Merlin")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("kontext-security/Merlin") model = AutoModelForSequenceClassification.from_pretrained("kontext-security/Merlin", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Add Merlin V3 ungated native evaluation release
Browse files- README.md +16 -0
- v3-ungated/LICENSE +201 -0
- v3-ungated/NOTICE +7 -0
- v3-ungated/README.md +84 -0
- v3-ungated/evaluation.json +60 -0
- v3-ungated/native/cmd/merlin/main.go +52 -0
- v3-ungated/native/go.mod +8 -0
- v3-ungated/native/go.sum +4 -0
- v3-ungated/native/stepsafety/agreement.go +145 -0
- v3-ungated/native/stepsafety/context.go +247 -0
- v3-ungated/native/stepsafety/model/ATTRIBUTION.md +1 -0
- v3-ungated/native/stepsafety/model/LICENSE +21 -0
- v3-ungated/native/stepsafety/model/PROVENANCE.json +89 -0
- v3-ungated/native/stepsafety/model/native/PROVENANCE.json +83 -0
- v3-ungated/native/stepsafety/model/native/agreement.json.gz +3 -0
- v3-ungated/native/stepsafety/model/native/candidate.json +147 -0
- v3-ungated/native/stepsafety/model/native/manifest.json +49 -0
- v3-ungated/native/stepsafety/model/native/request_gate.json.gz +3 -0
- v3-ungated/native/stepsafety/model/native/tokenizer.json.gz +3 -0
- v3-ungated/native/stepsafety/model/native/weights.00.gz +3 -0
- v3-ungated/native/stepsafety/model/native/weights.01.gz +3 -0
- v3-ungated/native/stepsafety/model/native/weights.02.gz +3 -0
- v3-ungated/native/stepsafety/model/native/weights.03.gz +3 -0
- v3-ungated/native/stepsafety/model/native/weights.04.gz +3 -0
- v3-ungated/native/stepsafety/modelstore.go +296 -0
- v3-ungated/native/stepsafety/native_assets.go +15 -0
- v3-ungated/native/stepsafety/native_assets_nomodel.go +14 -0
- v3-ungated/native/stepsafety/native_backend.go +133 -0
- v3-ungated/native/stepsafety/native_linear_arm64.go +82 -0
- v3-ungated/native/stepsafety/native_linear_arm64.s +57 -0
- v3-ungated/native/stepsafety/native_linear_generic.go +20 -0
- v3-ungated/native/stepsafety/native_model.go +288 -0
- v3-ungated/native/stepsafety/native_weights.go +130 -0
- v3-ungated/native/stepsafety/native_workspace.go +31 -0
- v3-ungated/native/stepsafety/normalization.go +187 -0
- v3-ungated/native/stepsafety/packing.go +215 -0
- v3-ungated/native/stepsafety/request_gate.go +122 -0
- v3-ungated/native/stepsafety/runtime_artifacts.go +11 -0
- v3-ungated/native/stepsafety/runtime_store.go +204 -0
- v3-ungated/native/stepsafety/scoped.go +164 -0
- v3-ungated/native/stepsafety/tokenizer.go +122 -0
- v3-ungated/native/stepsafety/types.go +468 -0
- v3-ungated/native/stepsafety/unicode9.go +79 -0
- v3-ungated/native/stepsafety/unigram.go +167 -0
- v3-ungated/release_manifest.json +180 -0
README.md
CHANGED
|
@@ -12,6 +12,22 @@ tags:
|
|
| 12 |
- deberta-v3
|
| 13 |
---
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# Merlin
|
| 16 |
|
| 17 |
Merlin is a 70.8M-parameter local encoder that classifies a proposed AI-agent
|
|
|
|
| 12 |
- deberta-v3
|
| 13 |
---
|
| 14 |
|
| 15 |
+
<!-- merlin-v3-ungated-start -->
|
| 16 |
+
## V3 ungated native evaluation release
|
| 17 |
+
|
| 18 |
+
The [V3 native Go package](v3-ungated/README.md) is available in `v3-ungated/`.
|
| 19 |
+
It keeps the V3 weights, threshold and action-agreement filter while removing the
|
| 20 |
+
three restrictive eligibility gates. AgentDojo recall is 24.1% (85/352), with
|
| 21 |
+
20/868 benign cases flagged. See the package for coverage, private-data aggregate
|
| 22 |
+
results and limitations. No private evaluation examples are included.
|
| 23 |
+
|
| 24 |
+
The root checkpoint and Python usage below remain **legacy v0.1**, with their
|
| 25 |
+
original results. They are not the V3 runtime. This is a controlled evaluation
|
| 26 |
+
release, not a claim of uniform performance across deployments.
|
| 27 |
+
<!-- merlin-v3-ungated-end -->
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
# Merlin
|
| 32 |
|
| 33 |
Merlin is a 70.8M-parameter local encoder that classifies a proposed AI-agent
|
v3-ungated/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
v3-ungated/NOTICE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Merlin
|
| 2 |
+
Copyright 2026 Kontext Security
|
| 3 |
+
|
| 4 |
+
The released checkpoint is fine-tuned from microsoft/deberta-v3-xsmall, which is
|
| 5 |
+
distributed under the MIT License. TS-Bench and its component datasets retain
|
| 6 |
+
their upstream terms. See DATASETS.md and the Hugging Face model card.
|
| 7 |
+
|
v3-ungated/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: merlin-research-release
|
| 4 |
+
base_model: microsoft/deberta-v3-xsmall
|
| 5 |
+
tags:
|
| 6 |
+
- agents
|
| 7 |
+
- agent-safety
|
| 8 |
+
- tool-use
|
| 9 |
+
- native-go
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Merlin V3 ungated — local evaluation release
|
| 13 |
+
|
| 14 |
+
Runtime `merlin-v3-ungated-20261104` uses the unchanged V3 contextual encoder and
|
| 15 |
+
sparse action-agreement model from `merlin-v3-precision-20261007`. This is a
|
| 16 |
+
runtime eligibility change, not newly trained weights. It removes the operation
|
| 17 |
+
allowlist, lexical request gate and learned informational-request gate.
|
| 18 |
+
|
| 19 |
+
The threshold remains **0.5**. The complete current action must fit **128 tokens**;
|
| 20 |
+
long actions and excluded tools remain unassessed. The output score is a
|
| 21 |
+
threshold-relative agreement score, **not a calibrated probability of risk**.
|
| 22 |
+
This is an advisory detector; it does not execute or block tool calls.
|
| 23 |
+
|
| 24 |
+
## Run locally
|
| 25 |
+
|
| 26 |
+
Download this folder (`v3-ungated`) from `kontext-security/Merlin`, preserving its
|
| 27 |
+
layout. Go 1.26.10 or later is required to build the standalone native runtime.
|
| 28 |
+
It embeds the supplied assets and needs no Python, ONNX runtime or hosted inference.
|
| 29 |
+
Go downloads normal build dependencies on the first build.
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
hf download kontext-security/Merlin --include 'v3-ungated/*' --local-dir dist/Merlin
|
| 33 |
+
cd dist/Merlin/v3-ungated/native
|
| 34 |
+
go build -o merlin ./cmd/merlin
|
| 35 |
+
printf '%s\n' '{"id":"example","input":{"user_request":"Find public documentation","tool_name":"WebSearch","tool_arguments":{"query":"Go JSON decoder documentation"},"interaction_history":"[]"}}' | ./merlin
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Each input line contains `id` and `input`. Input fields are `user_request`,
|
| 39 |
+
`interaction_history` (a JSON array encoded as a string), `tool_name`,
|
| 40 |
+
`tool_arguments`, and optionally `available_tool_schemas`. Include only completed
|
| 41 |
+
**prior** tool observations in history, never the current result or later turns.
|
| 42 |
+
Each output line preserves `id` and includes `evaluation`; unavailable assessments
|
| 43 |
+
have an `error_code` and no score. Do not treat those as safe classifications.
|
| 44 |
+
|
| 45 |
+
## Evaluation
|
| 46 |
+
|
| 47 |
+
| Cohort | Assessed / available | Detections or alerts |
|
| 48 |
+
|---|---:|---|
|
| 49 |
+
| AgentDojo-Traj | 1,159 / 1,220 | 85/352 positives detected (24.1% recall); 20/868 benign cases flagged (2.3% FPR) |
|
| 50 |
+
| Private historical regression | 415 / 610 | 1 false alert |
|
| 51 |
+
| Private later workload sample | 600 / 1,207 | 3 clear false alerts, 1 uncertain low-impact alert |
|
| 52 |
+
|
| 53 |
+
The original collected false-positive subset has 539 saved cases, of which 409
|
| 54 |
+
were reconstructed and assessed: Claude Code 0/128 alerts; Codex 1/281. The expanded
|
| 55 |
+
historical panel above includes these cases. These cohorts are not additive.
|
| 56 |
+
The later sample's clear false-alert frequency is 3/600 assessed calls (0.50%);
|
| 57 |
+
607 cases remain unassessed. This is not a population false-positive guarantee.
|
| 58 |
+
|
| 59 |
+
Historical data overlap training/selection/calibration and AgentDojo was repeatedly
|
| 60 |
+
consulted during development. The later sample is stratified, not traffic weighted.
|
| 61 |
+
Historical context was reconstructed from transcripts rather than exact daemon
|
| 62 |
+
snapshots. The remaining historical examples are unresolved, not successful tests.
|
| 63 |
+
No fresh ungated ASB or AgentHarm result is claimed. The earlier v0.1 benchmark
|
| 64 |
+
numbers do not describe this runtime.
|
| 65 |
+
|
| 66 |
+
This release is suitable for controlled evaluation. It is not evidence of uniform
|
| 67 |
+
precision on unfamiliar tools or workloads. See `evaluation.json` for aggregates
|
| 68 |
+
and `release_manifest.json` for file checksums. **No private prompts, calls,
|
| 69 |
+
transcripts, reviewer text, identifiers or per-example scores are distributed.**
|
| 70 |
+
Private evaluation requires the internal access-controlled archive; the public
|
| 71 |
+
release cannot independently reproduce its private-data metrics.
|
| 72 |
+
|
| 73 |
+
## Provenance and terms
|
| 74 |
+
|
| 75 |
+
Base encoder: `microsoft/deberta-v3-xsmall` (MIT), revision
|
| 76 |
+
`4b419818330868dff6a60ad3e6b1c730f8b8c0c6`. The V3 encoder weights SHA-256 is
|
| 77 |
+
`429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1`.
|
| 78 |
+
V3 was developed using benchmark data and private training/selection examples;
|
| 79 |
+
only model parameters and aggregate evaluation results are released here.
|
| 80 |
+
Upstream dataset/model terms continue to apply; consult the bundled attribution
|
| 81 |
+
and provenance files. The checkpoint is a research release (`other` license),
|
| 82 |
+
not a blanket commercial-use license. Runtime source follows the repository
|
| 83 |
+
Apache-2.0 license. The older request-intent asset remains for provenance but is
|
| 84 |
+
not used by the ungated inference path.
|
v3-ungated/evaluation.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"runtime": "merlin-v3-ungated-20261104",
|
| 3 |
+
"weights_candidate": "merlin-v3-precision-20261007",
|
| 4 |
+
"threshold": 0.5,
|
| 5 |
+
"action_token_limit": 128,
|
| 6 |
+
"action_agreement": true,
|
| 7 |
+
"removed_gates": [
|
| 8 |
+
"operation_allowlist",
|
| 9 |
+
"lexical_request",
|
| 10 |
+
"learned_request_intent"
|
| 11 |
+
],
|
| 12 |
+
"AgentDojo": {
|
| 13 |
+
"total": 1220,
|
| 14 |
+
"assessed": 1159,
|
| 15 |
+
"tp": 85,
|
| 16 |
+
"fp": 20,
|
| 17 |
+
"positives": 352,
|
| 18 |
+
"negatives": 868
|
| 19 |
+
},
|
| 20 |
+
"private_historical_regression": {
|
| 21 |
+
"total": 415,
|
| 22 |
+
"assessed": 415,
|
| 23 |
+
"skipped": 0,
|
| 24 |
+
"alerts": 1
|
| 25 |
+
},
|
| 26 |
+
"private_original_flagged_no_risk": {
|
| 27 |
+
"saved": 539,
|
| 28 |
+
"reconstructed_and_assessed": 409,
|
| 29 |
+
"alerts": 1,
|
| 30 |
+
"claude_code": {
|
| 31 |
+
"saved": 133,
|
| 32 |
+
"assessed": 128,
|
| 33 |
+
"alerts": 0
|
| 34 |
+
},
|
| 35 |
+
"codex": {
|
| 36 |
+
"saved": 406,
|
| 37 |
+
"assessed": 281,
|
| 38 |
+
"alerts": 1
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"private_recent_sample": {
|
| 42 |
+
"total": 1207,
|
| 43 |
+
"assessed": 600,
|
| 44 |
+
"skipped": 607,
|
| 45 |
+
"alerts": 4
|
| 46 |
+
},
|
| 47 |
+
"private_recent_alert_review": {
|
| 48 |
+
"clear_false_alerts": 3,
|
| 49 |
+
"uncertain_low_impact": 1,
|
| 50 |
+
"confirmed_true_positive": 0
|
| 51 |
+
},
|
| 52 |
+
"limitations": [
|
| 53 |
+
"Historical examples overlap model development; regression evidence, not unseen holdout.",
|
| 54 |
+
"AgentDojo was repeatedly consulted during development.",
|
| 55 |
+
"Recent sample is stratified and reconstructed from pre-call transcripts; not representative of every deployment.",
|
| 56 |
+
"Unassessed examples are not safe predictions.",
|
| 57 |
+
"No fresh ungated ASB or AgentHarm result is claimed."
|
| 58 |
+
],
|
| 59 |
+
"private_examples_published": false
|
| 60 |
+
}
|
v3-ungated/native/cmd/merlin/main.go
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package main
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bufio"
|
| 5 |
+
"context"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"fmt"
|
| 8 |
+
"os"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"github.com/kontext-security/merlin/native/stepsafety"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// Merlin only evaluates these inputs; it never executes the supplied tool calls.
|
| 15 |
+
func main() {
|
| 16 |
+
ctx := context.Background()
|
| 17 |
+
evaluator := stepsafety.New(ctx, stepsafety.Config{Enabled: true, Timeout: 30 * time.Second, StartupTimeout: 30 * time.Second, MaxConcurrency: 1})
|
| 18 |
+
health := evaluator.Health(ctx)
|
| 19 |
+
if health.Status != "ready" {
|
| 20 |
+
fmt.Fprintln(os.Stderr, "Merlin initialization failed:", health.ErrorCode)
|
| 21 |
+
os.Exit(1)
|
| 22 |
+
}
|
| 23 |
+
scan := bufio.NewScanner(os.Stdin)
|
| 24 |
+
scan.Buffer(make([]byte, 65536), 4*1024*1024)
|
| 25 |
+
output := json.NewEncoder(os.Stdout)
|
| 26 |
+
for scan.Scan() {
|
| 27 |
+
var row struct {
|
| 28 |
+
ID string `json:"id"`
|
| 29 |
+
Input stepsafety.Input `json:"input"`
|
| 30 |
+
}
|
| 31 |
+
if err := json.Unmarshal(scan.Bytes(), &row); err != nil {
|
| 32 |
+
fmt.Fprintln(os.Stderr, "Invalid input JSON:", err)
|
| 33 |
+
os.Exit(1)
|
| 34 |
+
}
|
| 35 |
+
if row.Input.ToolName == "" {
|
| 36 |
+
fmt.Fprintln(os.Stderr, "input.tool_name is required")
|
| 37 |
+
os.Exit(1)
|
| 38 |
+
}
|
| 39 |
+
result := struct {
|
| 40 |
+
ID string `json:"id"`
|
| 41 |
+
Evaluation stepsafety.Evaluation `json:"evaluation"`
|
| 42 |
+
}{row.ID, evaluator.Evaluate(ctx, row.Input)}
|
| 43 |
+
if err := output.Encode(result); err != nil {
|
| 44 |
+
fmt.Fprintln(os.Stderr, err)
|
| 45 |
+
os.Exit(1)
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
if err := scan.Err(); err != nil {
|
| 49 |
+
fmt.Fprintln(os.Stderr, err)
|
| 50 |
+
os.Exit(1)
|
| 51 |
+
}
|
| 52 |
+
}
|
v3-ungated/native/go.mod
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module github.com/kontext-security/merlin/native
|
| 2 |
+
|
| 3 |
+
go 1.26.10
|
| 4 |
+
|
| 5 |
+
require (
|
| 6 |
+
golang.org/x/text v0.43.0
|
| 7 |
+
gonum.org/v1/gonum v0.17.0
|
| 8 |
+
)
|
v3-ungated/native/go.sum
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
| 2 |
+
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
| 3 |
+
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
| 4 |
+
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
v3-ungated/native/stepsafety/agreement.go
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"compress/gzip"
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/sha256"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"errors"
|
| 10 |
+
"golang.org/x/text/cases"
|
| 11 |
+
"golang.org/x/text/language"
|
| 12 |
+
"io"
|
| 13 |
+
"math"
|
| 14 |
+
"strings"
|
| 15 |
+
"unicode"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
type sparseVector struct {
|
| 19 |
+
Features map[string][2]float64 `json:"features"`
|
| 20 |
+
}
|
| 21 |
+
type scoreCalibration struct{ Coefficient, Intercept float64 }
|
| 22 |
+
|
| 23 |
+
var agreementModel struct {
|
| 24 |
+
Word, Char sparseVector
|
| 25 |
+
Intercept float64
|
| 26 |
+
EncoderCalibration scoreCalibration `json:"encoder_calibrator"`
|
| 27 |
+
SparseCalibration scoreCalibration `json:"sparse_calibrator"`
|
| 28 |
+
EncoderThreshold float64 `json:"encoder_threshold"`
|
| 29 |
+
SparseThreshold float64 `json:"sparse_threshold"`
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func loadAgreementAssets() error {
|
| 33 |
+
f, err := nativeAssets.Open("model/native/agreement.json.gz")
|
| 34 |
+
if err != nil {
|
| 35 |
+
return err
|
| 36 |
+
}
|
| 37 |
+
defer f.Close()
|
| 38 |
+
reader, err := gzip.NewReader(f)
|
| 39 |
+
if err != nil {
|
| 40 |
+
return err
|
| 41 |
+
}
|
| 42 |
+
defer reader.Close()
|
| 43 |
+
data, err := io.ReadAll(io.LimitReader(reader, 4918863+1))
|
| 44 |
+
if err != nil {
|
| 45 |
+
return err
|
| 46 |
+
}
|
| 47 |
+
h := sha256.Sum256(data)
|
| 48 |
+
if len(data) != 4918863 || hex.EncodeToString(h[:]) != "7f01698e14f36f72ccad7f519c4e95298f125c75c469ef25b4b0f08052f1dcc8" {
|
| 49 |
+
return errors.New("agreement asset checksum mismatch")
|
| 50 |
+
}
|
| 51 |
+
return json.Unmarshal(data, &agreementModel)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
func vectorScore(counts map[string]int, vector sparseVector) float64 {
|
| 55 |
+
var norm, dot float64
|
| 56 |
+
for term, count := range counts {
|
| 57 |
+
h := sha256.Sum256([]byte(term))
|
| 58 |
+
feature, ok := vector.Features[hex.EncodeToString(h[:])]
|
| 59 |
+
if !ok {
|
| 60 |
+
continue
|
| 61 |
+
}
|
| 62 |
+
value := (1 + math.Log(float64(count))) * feature[0]
|
| 63 |
+
norm += value * value
|
| 64 |
+
dot += value * feature[1]
|
| 65 |
+
}
|
| 66 |
+
if norm == 0 {
|
| 67 |
+
return 0
|
| 68 |
+
}
|
| 69 |
+
return dot / math.Sqrt(norm)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func pythonSpace(r rune) bool { return unicode.IsSpace(r) || (r >= 0x1c && r <= 0x1f) }
|
| 73 |
+
|
| 74 |
+
func sparseMargin(text string) float64 {
|
| 75 |
+
// Match sklearn's lowercase Unicode word analyzer and char analyzer.
|
| 76 |
+
// The vocabulary is hashed to avoid shipping plaintext captured tokens.
|
| 77 |
+
text = cases.Lower(language.Und).String(text)
|
| 78 |
+
words := []string{}
|
| 79 |
+
run := []rune{}
|
| 80 |
+
flush := func() {
|
| 81 |
+
if len(run) >= 2 {
|
| 82 |
+
words = append(words, string(run))
|
| 83 |
+
}
|
| 84 |
+
run = run[:0]
|
| 85 |
+
}
|
| 86 |
+
for _, r := range text {
|
| 87 |
+
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
|
| 88 |
+
run = append(run, r)
|
| 89 |
+
} else {
|
| 90 |
+
flush()
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
flush()
|
| 94 |
+
counts := map[string]int{}
|
| 95 |
+
for i, word := range words {
|
| 96 |
+
counts[word]++
|
| 97 |
+
if i > 0 {
|
| 98 |
+
counts[words[i-1]+" "+word]++
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
score := agreementModel.Intercept + vectorScore(counts, agreementModel.Word)
|
| 102 |
+
var normalized strings.Builder
|
| 103 |
+
rr := []rune(text)
|
| 104 |
+
for i := 0; i < len(rr); {
|
| 105 |
+
j := i + 1
|
| 106 |
+
if pythonSpace(rr[i]) {
|
| 107 |
+
for j < len(rr) && pythonSpace(rr[j]) {
|
| 108 |
+
j++
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
if j-i >= 2 {
|
| 112 |
+
normalized.WriteByte(' ')
|
| 113 |
+
} else {
|
| 114 |
+
normalized.WriteRune(rr[i])
|
| 115 |
+
}
|
| 116 |
+
i = j
|
| 117 |
+
}
|
| 118 |
+
rr = []rune(normalized.String())
|
| 119 |
+
counts = map[string]int{}
|
| 120 |
+
for n := 3; n <= 5; n++ {
|
| 121 |
+
for i := 0; i+n <= len(rr); i++ {
|
| 122 |
+
counts[string(rr[i:i+n])]++
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
return score + vectorScore(counts, agreementModel.Char)
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
func applyAgreement(ctx context.Context, input Input, original InferenceResult) (InferenceResult, error) {
|
| 129 |
+
if err := ctx.Err(); err != nil {
|
| 130 |
+
return InferenceResult{}, err
|
| 131 |
+
}
|
| 132 |
+
fields, err := candidateFields(input)
|
| 133 |
+
if err != nil {
|
| 134 |
+
return InferenceResult{}, err
|
| 135 |
+
}
|
| 136 |
+
e, s := agreementModel.EncoderCalibration, agreementModel.SparseCalibration
|
| 137 |
+
et, st := agreementModel.EncoderThreshold, agreementModel.SparseThreshold
|
| 138 |
+
encoder := e.Coefficient*(original.Logits[1]-original.Logits[0]) + e.Intercept - math.Log(et/(1-et))
|
| 139 |
+
sparse := s.Coefficient*sparseMargin(fields[2]) + s.Intercept - math.Log(st/(1-st))
|
| 140 |
+
if err := ctx.Err(); err != nil {
|
| 141 |
+
return InferenceResult{}, err
|
| 142 |
+
}
|
| 143 |
+
original.Logits = [2]float64{0, math.Min(encoder, sparse)}
|
| 144 |
+
return original, nil
|
| 145 |
+
}
|
v3-ungated/native/stepsafety/context.go
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"strings"
|
| 7 |
+
"sync"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
const (
|
| 11 |
+
maxContextSessions = 256
|
| 12 |
+
maxHistoryEntries = 24
|
| 13 |
+
maxHistoryBytes = maxPretokenizedFieldBytes
|
| 14 |
+
// Retain results that fit the history field so token packing can keep their
|
| 15 |
+
// beginning and end. The total per-session bound remains maxHistoryBytes.
|
| 16 |
+
maxHistoryEntryBytes = maxHistoryBytes
|
| 17 |
+
maxUserRequestBytes = maxPretokenizedFieldBytes
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
type HistoryEntry struct {
|
| 21 |
+
ToolName string
|
| 22 |
+
ToolArguments map[string]any
|
| 23 |
+
ToolResponse map[string]any
|
| 24 |
+
Error string
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
type sessionContext struct {
|
| 28 |
+
request string
|
| 29 |
+
history []string
|
| 30 |
+
historyOmitted bool
|
| 31 |
+
requestTooLarge bool
|
| 32 |
+
serial uint64
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// ContextStore keeps only production-visible, structured hook data. It never
|
| 36 |
+
// reads transcripts or assistant messages, so agent Thought/reasoning cannot
|
| 37 |
+
// enter the model through this history path. The store is memory-only and
|
| 38 |
+
// bounded; raw context is never part of telemetry.
|
| 39 |
+
type ContextStore struct {
|
| 40 |
+
mu sync.Mutex
|
| 41 |
+
sessions map[string]*sessionContext
|
| 42 |
+
serial uint64
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
func NewContextStore() *ContextStore {
|
| 46 |
+
return &ContextStore{sessions: make(map[string]*sessionContext)}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
func (s *ContextStore) RecordUserRequest(sessionID, request string) {
|
| 50 |
+
if s == nil || sessionID == "" {
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
s.mu.Lock()
|
| 54 |
+
defer s.mu.Unlock()
|
| 55 |
+
ctx := s.session(sessionID)
|
| 56 |
+
ctx.requestTooLarge = len(request) > maxUserRequestBytes
|
| 57 |
+
if ctx.requestTooLarge {
|
| 58 |
+
ctx.request = ""
|
| 59 |
+
} else {
|
| 60 |
+
ctx.request = strings.Clone(request)
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func (s *ContextStore) RecordInteraction(sessionID string, entry HistoryEntry) {
|
| 65 |
+
if s == nil || sessionID == "" || entry.ToolName == "" {
|
| 66 |
+
return
|
| 67 |
+
}
|
| 68 |
+
s.mu.Lock()
|
| 69 |
+
defer s.mu.Unlock()
|
| 70 |
+
ctx := s.session(sessionID)
|
| 71 |
+
if ExcludedTool(entry.ToolName) {
|
| 72 |
+
ctx.historyOmitted = true
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
remaining := maxHistoryEntryBytes
|
| 76 |
+
for _, value := range []any{entry.ToolName, entry.ToolArguments, entry.ToolResponse, entry.Error} {
|
| 77 |
+
if err := boundedJSONSize(value, &remaining, 0); err != nil {
|
| 78 |
+
ctx.historyOmitted = true
|
| 79 |
+
return
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
encoded, err := serializeHistory([]HistoryEntry{entry})
|
| 83 |
+
if err != nil || len(encoded) > maxHistoryEntryBytes {
|
| 84 |
+
ctx.historyOmitted = true
|
| 85 |
+
return
|
| 86 |
+
}
|
| 87 |
+
// Store an immutable, bounded serialization, never references to caller
|
| 88 |
+
// maps or large raw tool results. Drop large events before evicting history.
|
| 89 |
+
ctx.history = append(ctx.history, encoded[1:len(encoded)-1])
|
| 90 |
+
for len(ctx.history) > maxHistoryEntries || len(joinHistory(ctx.history)) > maxHistoryBytes {
|
| 91 |
+
ctx.history = append([]string(nil), ctx.history[1:]...)
|
| 92 |
+
ctx.historyOmitted = true
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
func joinHistory(entries []string) string { return "[" + strings.Join(entries, ",") + "]" }
|
| 97 |
+
|
| 98 |
+
type ContextSnapshot struct {
|
| 99 |
+
UserRequest string
|
| 100 |
+
InteractionHistory string
|
| 101 |
+
HistoryOmitted bool
|
| 102 |
+
RequestTooLarge bool
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
func (s *ContextStore) Snapshot(sessionID string) (userRequest, interactionHistory string) {
|
| 106 |
+
snapshot := s.SnapshotWithCoverage(sessionID)
|
| 107 |
+
return snapshot.UserRequest, snapshot.InteractionHistory
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
func (s *ContextStore) SnapshotWithCoverage(sessionID string) ContextSnapshot {
|
| 111 |
+
empty := ContextSnapshot{InteractionHistory: "[]"}
|
| 112 |
+
if s == nil || sessionID == "" {
|
| 113 |
+
return empty
|
| 114 |
+
}
|
| 115 |
+
s.mu.Lock()
|
| 116 |
+
defer s.mu.Unlock()
|
| 117 |
+
ctx := s.sessions[sessionID]
|
| 118 |
+
if ctx == nil {
|
| 119 |
+
return empty
|
| 120 |
+
}
|
| 121 |
+
s.serial++
|
| 122 |
+
ctx.serial = s.serial
|
| 123 |
+
return ContextSnapshot{
|
| 124 |
+
UserRequest: ctx.request, InteractionHistory: joinHistory(ctx.history),
|
| 125 |
+
HistoryOmitted: ctx.historyOmitted, RequestTooLarge: ctx.requestTooLarge,
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// serializeHistory implements the exact structured-history representation used
|
| 130 |
+
// to train the v2 encoder: a compact, key-sorted JSON array with tool,
|
| 131 |
+
// arguments, and string observation fields. Empty history is always [].
|
| 132 |
+
// Tool results remain inert text so their contents can never become a trusted
|
| 133 |
+
// instruction channel through a richer runtime type.
|
| 134 |
+
func serializeHistory(entries []HistoryEntry) (string, error) {
|
| 135 |
+
events := make([]map[string]any, 0, len(entries))
|
| 136 |
+
for _, entry := range entries {
|
| 137 |
+
if strings.TrimSpace(entry.ToolName) == "" {
|
| 138 |
+
continue
|
| 139 |
+
}
|
| 140 |
+
event := map[string]any{"tool": entry.ToolName}
|
| 141 |
+
if entry.ToolArguments != nil {
|
| 142 |
+
event["arguments"] = entry.ToolArguments
|
| 143 |
+
}
|
| 144 |
+
if observation, ok, err := observationText(entry.ToolResponse, entry.Error); err != nil {
|
| 145 |
+
return "", err
|
| 146 |
+
} else if ok {
|
| 147 |
+
event["observation"] = observation
|
| 148 |
+
}
|
| 149 |
+
events = append(events, event)
|
| 150 |
+
}
|
| 151 |
+
return compactSortedJSON(events)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
func observationText(response map[string]any, errorText string) (string, bool, error) {
|
| 155 |
+
if response == nil {
|
| 156 |
+
if errorText == "" {
|
| 157 |
+
return "", false, nil
|
| 158 |
+
}
|
| 159 |
+
return errorText, true, nil
|
| 160 |
+
}
|
| 161 |
+
if errorText == "" {
|
| 162 |
+
encoded, err := compactSortedJSON(response)
|
| 163 |
+
return encoded, true, err
|
| 164 |
+
}
|
| 165 |
+
encoded, err := compactSortedJSON(map[string]any{
|
| 166 |
+
"error": errorText,
|
| 167 |
+
"response": response,
|
| 168 |
+
})
|
| 169 |
+
return encoded, true, err
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// compactSortedJSON matches Python json.dumps(..., ensure_ascii=False,
|
| 173 |
+
// sort_keys=True, separators=(",", ":")) for the JSON values admitted by the
|
| 174 |
+
// hook adapters. encoding/json sorts object keys; disabling HTML escaping keeps
|
| 175 |
+
// ordinary Unicode and symbols byte-compatible with the training proxy.
|
| 176 |
+
func compactSortedJSON(value any) (string, error) {
|
| 177 |
+
var output bytes.Buffer
|
| 178 |
+
encoder := json.NewEncoder(&output)
|
| 179 |
+
encoder.SetEscapeHTML(false)
|
| 180 |
+
if err := encoder.Encode(value); err != nil {
|
| 181 |
+
return "", err
|
| 182 |
+
}
|
| 183 |
+
return unescapeJSONLineSeparators(strings.TrimSuffix(output.String(), "\n")), nil
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
// encoding/json always escapes U+2028 and U+2029 for legacy JSONP safety,
|
| 187 |
+
// while Python ensure_ascii=False keeps them as UTF-8. Only decode genuine JSON
|
| 188 |
+
// escapes here; an input string containing the literal text `\u2028` must keep
|
| 189 |
+
// its leading backslash.
|
| 190 |
+
func unescapeJSONLineSeparators(value string) string {
|
| 191 |
+
if !strings.Contains(value, `\u2028`) && !strings.Contains(value, `\u2029`) {
|
| 192 |
+
return value
|
| 193 |
+
}
|
| 194 |
+
var output strings.Builder
|
| 195 |
+
output.Grow(len(value))
|
| 196 |
+
for index := 0; index < len(value); {
|
| 197 |
+
if index+6 <= len(value) && (value[index:index+6] == `\u2028` || value[index:index+6] == `\u2029`) {
|
| 198 |
+
precedingSlashes := 0
|
| 199 |
+
for prior := index - 1; prior >= 0 && value[prior] == '\\'; prior-- {
|
| 200 |
+
precedingSlashes++
|
| 201 |
+
}
|
| 202 |
+
if precedingSlashes%2 == 0 {
|
| 203 |
+
if value[index+5] == '8' {
|
| 204 |
+
output.WriteRune('\u2028')
|
| 205 |
+
} else {
|
| 206 |
+
output.WriteRune('\u2029')
|
| 207 |
+
}
|
| 208 |
+
index += 6
|
| 209 |
+
continue
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
output.WriteByte(value[index])
|
| 213 |
+
index++
|
| 214 |
+
}
|
| 215 |
+
return output.String()
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
func (s *ContextStore) CloseSession(sessionID string) {
|
| 219 |
+
if s == nil || sessionID == "" {
|
| 220 |
+
return
|
| 221 |
+
}
|
| 222 |
+
s.mu.Lock()
|
| 223 |
+
defer s.mu.Unlock()
|
| 224 |
+
delete(s.sessions, sessionID)
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
func (s *ContextStore) session(sessionID string) *sessionContext {
|
| 228 |
+
if existing := s.sessions[sessionID]; existing != nil {
|
| 229 |
+
s.serial++
|
| 230 |
+
existing.serial = s.serial
|
| 231 |
+
return existing
|
| 232 |
+
}
|
| 233 |
+
if len(s.sessions) >= maxContextSessions {
|
| 234 |
+
var oldestID string
|
| 235 |
+
var oldest uint64
|
| 236 |
+
for id, candidate := range s.sessions {
|
| 237 |
+
if oldestID == "" || candidate.serial < oldest {
|
| 238 |
+
oldestID, oldest = id, candidate.serial
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
delete(s.sessions, oldestID)
|
| 242 |
+
}
|
| 243 |
+
s.serial++
|
| 244 |
+
created := &sessionContext{serial: s.serial}
|
| 245 |
+
s.sessions[sessionID] = created
|
| 246 |
+
return created
|
| 247 |
+
}
|
v3-ungated/native/stepsafety/model/ATTRIBUTION.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
The contextual DeBERTa-v3-xsmall base is Microsoft MIT-licensed (see LICENSE in this directory). Kontext's fine-tuned checkpoint is combined with two independently trained sparse heads. The request-intent head is adapted from MASSIVE 1.1 en-US, Copyright Amazon.com Inc. or its affiliates, derived from SLURP, licensed CC BY 4.0: https://github.com/alexa/massive and https://creativecommons.org/licenses/by/4.0/ . Adaptations include a binary query-intent mapping, deduplication, TF-IDF feature extraction and fitted logistic weights. No raw MASSIVE utterances or local captures are included in this serving bundle.
|
v3-ungated/native/stepsafety/model/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) Microsoft Corporation.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE
|
v3-ungated/native/stepsafety/model/PROVENANCE.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 3,
|
| 3 |
+
"model_version": "toolsafe-deberta-v3-xsmall-onnx-scoped-v2",
|
| 4 |
+
"source_project": "kontext-security/toolsafe-lab",
|
| 5 |
+
"source_revision": "9c63e6191598b0ba72947a4394ac8297c41053d1",
|
| 6 |
+
"source_artifact_path": "artifacts/models/history_serialization/deberta_v3_xsmall",
|
| 7 |
+
"source_results_path": "results/history_serialization_ablation.json",
|
| 8 |
+
"source_results_sha256": "09efd898c02dedb1ad8f83dd5b90888e84cbcf8919c51450c9289176c42db41a",
|
| 9 |
+
"source_findings_path": "docs/HISTORY_SERIALIZATION_ABLATION_FINDINGS.md",
|
| 10 |
+
"source_findings_sha256": "f4231362a77e8d622ace51c4d8ac323fc801cca6ebc6771d5295b55df2bef08f",
|
| 11 |
+
"source_serializer_path": "src/toolsafe_lab/history_serialization.py",
|
| 12 |
+
"source_serializer_sha256": "4b14a72a6825555d56c4be0549039428b78332940d3cdd25516ef278888642e2",
|
| 13 |
+
"source_packing_path": "src/toolsafe_lab/standalone_encoder.py",
|
| 14 |
+
"source_packing_sha256": "ca8db9401025c8489a142d9447140a857fd77df84e5246fde92c995ccbe173ff",
|
| 15 |
+
"source_protocol_path": "docs/HISTORY_SERIALIZATION_ABLATION_PROTOCOL.md",
|
| 16 |
+
"source_protocol_sha256": "6e44e1175be44573e6feef1130ef9f4faeaf8457025c79d5100212c568099089",
|
| 17 |
+
"base_model": "microsoft/deberta-v3-xsmall",
|
| 18 |
+
"base_model_revision": "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
|
| 19 |
+
"base_model_license": "MIT",
|
| 20 |
+
"input_mode": "execution_context + canonical structured history (agent Thought excluded)",
|
| 21 |
+
"thought_included": false,
|
| 22 |
+
"history_serialization": {
|
| 23 |
+
"event_fields": [
|
| 24 |
+
"tool",
|
| 25 |
+
"arguments",
|
| 26 |
+
"observation"
|
| 27 |
+
],
|
| 28 |
+
"empty_history": "[]",
|
| 29 |
+
"observations": "strings",
|
| 30 |
+
"json_encoding": "UTF-8, sorted keys, compact separators"
|
| 31 |
+
},
|
| 32 |
+
"evaluation": {
|
| 33 |
+
"threshold": 0.5,
|
| 34 |
+
"accuracy": 0.9118629908103593,
|
| 35 |
+
"precision": 0.9266195885784464,
|
| 36 |
+
"recall": 0.8845252051582649,
|
| 37 |
+
"f1": 0.9050832208726945,
|
| 38 |
+
"mcc": 0.8236797425872459,
|
| 39 |
+
"worst_source_recall": 0.5085227272727273
|
| 40 |
+
},
|
| 41 |
+
"max_length": 512,
|
| 42 |
+
"field_markers": {
|
| 43 |
+
"request": "[USER_REQUEST]",
|
| 44 |
+
"history": "[INTERACTION_HISTORY]",
|
| 45 |
+
"action": "[CURRENT_ACTION]",
|
| 46 |
+
"schema": "[TOOL_DESCRIPTIONS]"
|
| 47 |
+
},
|
| 48 |
+
"field_budgets": {
|
| 49 |
+
"request": 96,
|
| 50 |
+
"history": 144,
|
| 51 |
+
"action": 128,
|
| 52 |
+
"schema": 128
|
| 53 |
+
},
|
| 54 |
+
"calibration_kind": "sigmoid",
|
| 55 |
+
"calibration_scale": 1.427213430140093,
|
| 56 |
+
"calibration_bias": 2.953687013257505,
|
| 57 |
+
"initial_threshold": 0.5,
|
| 58 |
+
"artifacts": [
|
| 59 |
+
{
|
| 60 |
+
"name": "config.json",
|
| 61 |
+
"size_bytes": 924,
|
| 62 |
+
"sha256": "41f89ae28c4f0549b92537d0ef5918ef9e1af25e9898af6456e3551bf0a93852"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"name": "model.onnx",
|
| 66 |
+
"size_bytes": 287233599,
|
| 67 |
+
"sha256": "7f7c068c80e826cee69fd3fca089662572b3372b0be109c6cf17b6852d94557c"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"name": "tokenizer.json",
|
| 71 |
+
"size_bytes": 8340654,
|
| 72 |
+
"sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "tokenizer_config.json",
|
| 76 |
+
"size_bytes": 601,
|
| 77 |
+
"sha256": "141468772d3b095cc30275fd9c05dff92a78619c341487da9f5549a3564ea6cb"
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"excluded_training_artifact": {
|
| 81 |
+
"name": "training_checkpoint.pt",
|
| 82 |
+
"reason": "optimizer/RNG training-resume state is not required for inference"
|
| 83 |
+
},
|
| 84 |
+
"runtime_format": "ONNX opset 17, float32, [1,512] int64 input_ids/attention_mask, [1,2] logits",
|
| 85 |
+
"input_policy": "scoped-v2: exclude file tools; whole request/action/schema or abstain; filtered history uses first 72 and last 72 tokens, matching training",
|
| 86 |
+
"source_weights_sha256": "075d68f8bb4c1b4b98e2d2b2c0d5b013056b202172b080b34e7364b698844c86",
|
| 87 |
+
"export_toolchain": "torch 2.13.0; transformers 5.14.1; onnx 1.22.0; legacy exporter; scripts/step-safety/export_onnx.py",
|
| 88 |
+
"evaluation_scope": "Original held-out TS-Bench checkpoint evaluation; not an estimate for scoped-v2 production coverage"
|
| 89 |
+
}
|
v3-ungated/native/stepsafety/model/native/PROVENANCE.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_version": "merlin-v3-ungated-20261104",
|
| 3 |
+
"source_weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
|
| 4 |
+
"tokenizer_sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031",
|
| 5 |
+
"experiment": "exp-20261007-merlin-v3-precision",
|
| 6 |
+
"candidate": "semantic_conservative",
|
| 7 |
+
"runtime": "Native Go FP32 DeBERTa plus native sparse TF-IDF logistic veto; all assets embedded, no ONNX or Python needed",
|
| 8 |
+
"base_model": "microsoft/deberta-v3-xsmall",
|
| 9 |
+
"base_revision": "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
|
| 10 |
+
"base_license": "MIT",
|
| 11 |
+
"scope": "All otherwise eligible tool invocations; operation allowlist, lexical authorization gate and learned request-intent gate removed",
|
| 12 |
+
"score_semantics": "Agreement margin rescaled to [0,1], not a calibrated production risk probability",
|
| 13 |
+
"context_budgets": [
|
| 14 |
+
96,
|
| 15 |
+
144,
|
| 16 |
+
128,
|
| 17 |
+
128
|
| 18 |
+
],
|
| 19 |
+
"context_truncation": "Request head; history/schema head-tail; current action never truncated",
|
| 20 |
+
"advisory_only": true,
|
| 21 |
+
"deployed": false,
|
| 22 |
+
"sparse_model_sha256": "cddc99dd145eeb4e29ebfb35bc874408c2737b4f340917919024dc7e9113c229",
|
| 23 |
+
"selection_sha256": "e682888797fb0f43bffffe0b31d196aa82be6195e65ad74cc52e61043eaa1b28",
|
| 24 |
+
"evaluation_sha256": "b2f7d0ee87a9bf30a5aeb80768401ade17df613e6de1befd764b5052160ee7a0",
|
| 25 |
+
"exporter_sha256": "a1847a55de609edfb18dbfab0ad00e863c9d10d46fa7e37127fb8724b69e2d64",
|
| 26 |
+
"request_gate": {
|
| 27 |
+
"model_sha256": "0ec73c392582b7f93a6c59c78e3153cd36be98d36cab73445f0f300dcb087528",
|
| 28 |
+
"threshold": 0.7871534193379106,
|
| 29 |
+
"training_source": "MASSIVE 1.1 en-US original train, CC BY 4.0",
|
| 30 |
+
"selection_sha256": "ab818785fe65d6207673ebb8841954928ec692d27d04d1077175d7de8a2899e1",
|
| 31 |
+
"calibrator": {
|
| 32 |
+
"coefficient": 0.937442667480181,
|
| 33 |
+
"intercept": 0.020245760734966128
|
| 34 |
+
},
|
| 35 |
+
"enabled": false
|
| 36 |
+
},
|
| 37 |
+
"authorization_additional_words": [
|
| 38 |
+
"adjust",
|
| 39 |
+
"approve",
|
| 40 |
+
"archive",
|
| 41 |
+
"buy",
|
| 42 |
+
"checkout",
|
| 43 |
+
"clear",
|
| 44 |
+
"commit",
|
| 45 |
+
"disburse",
|
| 46 |
+
"discard",
|
| 47 |
+
"dispose",
|
| 48 |
+
"drop",
|
| 49 |
+
"empty",
|
| 50 |
+
"forward",
|
| 51 |
+
"grant",
|
| 52 |
+
"invite",
|
| 53 |
+
"merge",
|
| 54 |
+
"move",
|
| 55 |
+
"order",
|
| 56 |
+
"post",
|
| 57 |
+
"publish",
|
| 58 |
+
"purchase",
|
| 59 |
+
"purge",
|
| 60 |
+
"push",
|
| 61 |
+
"refund",
|
| 62 |
+
"reimburse",
|
| 63 |
+
"rename",
|
| 64 |
+
"reply",
|
| 65 |
+
"reschedule",
|
| 66 |
+
"respond",
|
| 67 |
+
"restart",
|
| 68 |
+
"rotate",
|
| 69 |
+
"save",
|
| 70 |
+
"set",
|
| 71 |
+
"share",
|
| 72 |
+
"start",
|
| 73 |
+
"stop",
|
| 74 |
+
"submit",
|
| 75 |
+
"unlink",
|
| 76 |
+
"upload",
|
| 77 |
+
"wipe"
|
| 78 |
+
],
|
| 79 |
+
"runtime_variant": "v3-ungated-20261104",
|
| 80 |
+
"weights_candidate": "merlin-v3-precision-20261007",
|
| 81 |
+
"authorization_lexical_gate_enabled": false,
|
| 82 |
+
"operation_gate_enabled": false
|
| 83 |
+
}
|
v3-ungated/native/stepsafety/model/native/agreement.json.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf
|
| 3 |
+
size 2290715
|
v3-ungated/native/stepsafety/model/native/candidate.json
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"candidate": "merlin-v3-ungated-20261104",
|
| 3 |
+
"weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
|
| 4 |
+
"threshold": 0.5,
|
| 5 |
+
"calibrator": {
|
| 6 |
+
"coefficient": 1.0,
|
| 7 |
+
"intercept": 0.0
|
| 8 |
+
},
|
| 9 |
+
"agreement": {
|
| 10 |
+
"sparse_model_sha256": "cddc99dd145eeb4e29ebfb35bc874408c2737b4f340917919024dc7e9113c229",
|
| 11 |
+
"encoder_calibrator": {
|
| 12 |
+
"coefficient": 0.4128351080107227,
|
| 13 |
+
"intercept": -1.3945806128701024
|
| 14 |
+
},
|
| 15 |
+
"sparse_calibrator": {
|
| 16 |
+
"coefficient": 1.2411713915407203,
|
| 17 |
+
"intercept": 0.12969558567752842
|
| 18 |
+
},
|
| 19 |
+
"operating_point": {
|
| 20 |
+
"threshold": 0.5,
|
| 21 |
+
"encoder_threshold": 0.5455402148631912,
|
| 22 |
+
"sparse_threshold": 0.2,
|
| 23 |
+
"rank": [
|
| 24 |
+
0.900131845841785,
|
| 25 |
+
0.9841269841269841,
|
| 26 |
+
295,
|
| 27 |
+
0.5455402148631912,
|
| 28 |
+
0.2
|
| 29 |
+
],
|
| 30 |
+
"validation": {
|
| 31 |
+
"selection": {
|
| 32 |
+
"local_staging": {
|
| 33 |
+
"n": 163,
|
| 34 |
+
"tp": 0,
|
| 35 |
+
"fp": 0,
|
| 36 |
+
"fn": 0,
|
| 37 |
+
"tn": 163,
|
| 38 |
+
"precision": null,
|
| 39 |
+
"recall": null,
|
| 40 |
+
"fpr": 0.0
|
| 41 |
+
},
|
| 42 |
+
"ASB-Traj": {
|
| 43 |
+
"n": 88,
|
| 44 |
+
"tp": 32,
|
| 45 |
+
"fp": 0,
|
| 46 |
+
"fn": 2,
|
| 47 |
+
"tn": 54,
|
| 48 |
+
"precision": 1.0,
|
| 49 |
+
"recall": 0.9411764705882353,
|
| 50 |
+
"fpr": 0.0
|
| 51 |
+
},
|
| 52 |
+
"AgentAlign-Traj": {
|
| 53 |
+
"n": 111,
|
| 54 |
+
"tp": 62,
|
| 55 |
+
"fp": 1,
|
| 56 |
+
"fn": 6,
|
| 57 |
+
"tn": 42,
|
| 58 |
+
"precision": 0.9841269841269841,
|
| 59 |
+
"recall": 0.9117647058823529,
|
| 60 |
+
"fpr": 0.023255813953488372
|
| 61 |
+
},
|
| 62 |
+
"authored": {
|
| 63 |
+
"n": 78,
|
| 64 |
+
"tp": 0,
|
| 65 |
+
"fp": 0,
|
| 66 |
+
"fn": 36,
|
| 67 |
+
"tn": 42,
|
| 68 |
+
"precision": null,
|
| 69 |
+
"recall": 0.0,
|
| 70 |
+
"fpr": 0.0
|
| 71 |
+
},
|
| 72 |
+
"benchmark": {
|
| 73 |
+
"n": 199,
|
| 74 |
+
"tp": 94,
|
| 75 |
+
"fp": 1,
|
| 76 |
+
"fn": 8,
|
| 77 |
+
"tn": 96,
|
| 78 |
+
"precision": 0.9894736842105263,
|
| 79 |
+
"recall": 0.9215686274509803,
|
| 80 |
+
"fpr": 0.010309278350515464
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"calibration": {
|
| 84 |
+
"local_staging": {
|
| 85 |
+
"n": 132,
|
| 86 |
+
"tp": 0,
|
| 87 |
+
"fp": 0,
|
| 88 |
+
"fn": 0,
|
| 89 |
+
"tn": 132,
|
| 90 |
+
"precision": null,
|
| 91 |
+
"recall": null,
|
| 92 |
+
"fpr": 0.0
|
| 93 |
+
},
|
| 94 |
+
"ASB-Traj": {
|
| 95 |
+
"n": 56,
|
| 96 |
+
"tp": 23,
|
| 97 |
+
"fp": 0,
|
| 98 |
+
"fn": 2,
|
| 99 |
+
"tn": 31,
|
| 100 |
+
"precision": 1.0,
|
| 101 |
+
"recall": 0.92,
|
| 102 |
+
"fpr": 0.0
|
| 103 |
+
},
|
| 104 |
+
"AgentAlign-Traj": {
|
| 105 |
+
"n": 121,
|
| 106 |
+
"tp": 48,
|
| 107 |
+
"fp": 0,
|
| 108 |
+
"fn": 10,
|
| 109 |
+
"tn": 63,
|
| 110 |
+
"precision": 1.0,
|
| 111 |
+
"recall": 0.8275862068965517,
|
| 112 |
+
"fpr": 0.0
|
| 113 |
+
},
|
| 114 |
+
"authored": {
|
| 115 |
+
"n": 82,
|
| 116 |
+
"tp": 0,
|
| 117 |
+
"fp": 0,
|
| 118 |
+
"fn": 40,
|
| 119 |
+
"tn": 42,
|
| 120 |
+
"precision": null,
|
| 121 |
+
"recall": 0.0,
|
| 122 |
+
"fpr": 0.0
|
| 123 |
+
},
|
| 124 |
+
"benchmark": {
|
| 125 |
+
"n": 177,
|
| 126 |
+
"tp": 71,
|
| 127 |
+
"fp": 0,
|
| 128 |
+
"fn": 12,
|
| 129 |
+
"tn": 94,
|
| 130 |
+
"precision": 1.0,
|
| 131 |
+
"recall": 0.8554216867469879,
|
| 132 |
+
"fpr": 0.0
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
},
|
| 138 |
+
"default_enabled": true,
|
| 139 |
+
"score_semantics": "Threshold-relative agreement score; not a calibrated risk probability",
|
| 140 |
+
"weights_candidate": "merlin-v3-precision-20261007",
|
| 141 |
+
"runtime_gates": {
|
| 142 |
+
"operation": false,
|
| 143 |
+
"lexical_request": false,
|
| 144 |
+
"learned_request_intent": false,
|
| 145 |
+
"action_agreement": true
|
| 146 |
+
}
|
| 147 |
+
}
|
v3-ungated/native/stepsafety/model/native/manifest.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"format": "kontext-merlin-native-safetensors-f32/v1",
|
| 3 |
+
"source_weights_sha256": "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1",
|
| 4 |
+
"tokenizer_sha256": "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031",
|
| 5 |
+
"quantized": false,
|
| 6 |
+
"shard_uncompressed_bytes": 67108864,
|
| 7 |
+
"files": [
|
| 8 |
+
{
|
| 9 |
+
"name": "agreement.json.gz",
|
| 10 |
+
"size_bytes": 2290715,
|
| 11 |
+
"sha256": "6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"name": "request_gate.json.gz",
|
| 15 |
+
"size_bytes": 2492102,
|
| 16 |
+
"sha256": "acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"name": "tokenizer.json.gz",
|
| 20 |
+
"size_bytes": 1736709,
|
| 21 |
+
"sha256": "956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c"
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"name": "weights.00.gz",
|
| 25 |
+
"size_bytes": 46014808,
|
| 26 |
+
"sha256": "2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"name": "weights.01.gz",
|
| 30 |
+
"size_bytes": 41825526,
|
| 31 |
+
"sha256": "129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"name": "weights.02.gz",
|
| 35 |
+
"size_bytes": 42596160,
|
| 36 |
+
"sha256": "7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"name": "weights.03.gz",
|
| 40 |
+
"size_bytes": 62319867,
|
| 41 |
+
"sha256": "d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"name": "weights.04.gz",
|
| 45 |
+
"size_bytes": 13716366,
|
| 46 |
+
"sha256": "9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000"
|
| 47 |
+
}
|
| 48 |
+
]
|
| 49 |
+
}
|
v3-ungated/native/stepsafety/model/native/request_gate.json.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3
|
| 3 |
+
size 2492102
|
v3-ungated/native/stepsafety/model/native/tokenizer.json.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c
|
| 3 |
+
size 1736709
|
v3-ungated/native/stepsafety/model/native/weights.00.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad
|
| 3 |
+
size 46014808
|
v3-ungated/native/stepsafety/model/native/weights.01.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04
|
| 3 |
+
size 41825526
|
v3-ungated/native/stepsafety/model/native/weights.02.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e
|
| 3 |
+
size 42596160
|
v3-ungated/native/stepsafety/model/native/weights.03.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c
|
| 3 |
+
size 62319867
|
v3-ungated/native/stepsafety/model/native/weights.04.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000
|
| 3 |
+
size 13716366
|
v3-ungated/native/stepsafety/modelstore.go
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"crypto/sha256"
|
| 5 |
+
"encoding/hex"
|
| 6 |
+
"encoding/json"
|
| 7 |
+
"errors"
|
| 8 |
+
"fmt"
|
| 9 |
+
"io"
|
| 10 |
+
"os"
|
| 11 |
+
"path/filepath"
|
| 12 |
+
"sort"
|
| 13 |
+
"strings"
|
| 14 |
+
"time"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
const legacyModelVersion = "toolsafe-deberta-v3-xsmall-onnx-scoped-v2"
|
| 18 |
+
|
| 19 |
+
type artifactSpec struct {
|
| 20 |
+
Name string `json:"name"`
|
| 21 |
+
Size int64 `json:"size_bytes"`
|
| 22 |
+
SHA256 string `json:"sha256"`
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
var requiredArtifacts = []artifactSpec{
|
| 26 |
+
{Name: "config.json", Size: 924, SHA256: "41f89ae28c4f0549b92537d0ef5918ef9e1af25e9898af6456e3551bf0a93852"},
|
| 27 |
+
{Name: "model.onnx", Size: 287233599, SHA256: "7f7c068c80e826cee69fd3fca089662572b3372b0be109c6cf17b6852d94557c"},
|
| 28 |
+
{Name: "tokenizer.json", Size: 8340654, SHA256: "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031"},
|
| 29 |
+
{Name: "tokenizer_config.json", Size: 601, SHA256: "141468772d3b095cc30275fd9c05dff92a78619c341487da9f5549a3564ea6cb"},
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const (
|
| 33 |
+
sourceRevision = "9c63e6191598b0ba72947a4394ac8297c41053d1"
|
| 34 |
+
sourceArtifactPath = "artifacts/models/history_serialization/deberta_v3_xsmall"
|
| 35 |
+
sourceResultsPath = "results/history_serialization_ablation.json"
|
| 36 |
+
sourceResultsSHA256 = "09efd898c02dedb1ad8f83dd5b90888e84cbcf8919c51450c9289176c42db41a"
|
| 37 |
+
sourceFindingsPath = "docs/HISTORY_SERIALIZATION_ABLATION_FINDINGS.md"
|
| 38 |
+
sourceFindingsSHA256 = "f4231362a77e8d622ace51c4d8ac323fc801cca6ebc6771d5295b55df2bef08f"
|
| 39 |
+
sourceSerializerPath = "src/toolsafe_lab/history_serialization.py"
|
| 40 |
+
sourceSerializerSHA = "4b14a72a6825555d56c4be0549039428b78332940d3cdd25516ef278888642e2"
|
| 41 |
+
sourcePackingPath = "src/toolsafe_lab/standalone_encoder.py"
|
| 42 |
+
sourcePackingSHA = "ca8db9401025c8489a142d9447140a857fd77df84e5246fde92c995ccbe173ff"
|
| 43 |
+
sourceProtocolPath = "docs/HISTORY_SERIALIZATION_ABLATION_PROTOCOL.md"
|
| 44 |
+
sourceProtocolSHA = "6e44e1175be44573e6feef1130ef9f4faeaf8457025c79d5100212c568099089"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
const microsoftMITLicense = `MIT License
|
| 48 |
+
|
| 49 |
+
Copyright (c) Microsoft Corporation.
|
| 50 |
+
|
| 51 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 52 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 53 |
+
in the Software without restriction, including without limitation the rights
|
| 54 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 55 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 56 |
+
furnished to do so, subject to the following conditions:
|
| 57 |
+
|
| 58 |
+
The above copyright notice and this permission notice shall be included in all
|
| 59 |
+
copies or substantial portions of the Software.
|
| 60 |
+
|
| 61 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 62 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 63 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 64 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 65 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 66 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 67 |
+
SOFTWARE
|
| 68 |
+
`
|
| 69 |
+
|
| 70 |
+
type Provenance struct {
|
| 71 |
+
RuntimeFormat string `json:"runtime_format"`
|
| 72 |
+
InputPolicy string `json:"input_policy"`
|
| 73 |
+
SourceWeightsSHA256 string `json:"source_weights_sha256"`
|
| 74 |
+
ExportToolchain string `json:"export_toolchain"`
|
| 75 |
+
EvaluationScope string `json:"evaluation_scope"`
|
| 76 |
+
|
| 77 |
+
SchemaVersion int `json:"schema_version"`
|
| 78 |
+
ModelVersion string `json:"model_version"`
|
| 79 |
+
SourceProject string `json:"source_project"`
|
| 80 |
+
SourceRevision string `json:"source_revision"`
|
| 81 |
+
SourceArtifactPath string `json:"source_artifact_path,omitempty"`
|
| 82 |
+
SourceResultsPath string `json:"source_results_path,omitempty"`
|
| 83 |
+
SourceResultsSHA256 string `json:"source_results_sha256,omitempty"`
|
| 84 |
+
SourceFindingsPath string `json:"source_findings_path,omitempty"`
|
| 85 |
+
SourceFindingsSHA256 string `json:"source_findings_sha256,omitempty"`
|
| 86 |
+
SourceSerializerPath string `json:"source_serializer_path,omitempty"`
|
| 87 |
+
SourceSerializerSHA string `json:"source_serializer_sha256,omitempty"`
|
| 88 |
+
SourcePackingPath string `json:"source_packing_path,omitempty"`
|
| 89 |
+
SourcePackingSHA string `json:"source_packing_sha256,omitempty"`
|
| 90 |
+
SourceProtocolPath string `json:"source_protocol_path,omitempty"`
|
| 91 |
+
SourceProtocolSHA string `json:"source_protocol_sha256,omitempty"`
|
| 92 |
+
BaseModel string `json:"base_model"`
|
| 93 |
+
BaseModelRevision string `json:"base_model_revision"`
|
| 94 |
+
BaseModelLicense string `json:"base_model_license"`
|
| 95 |
+
InputMode string `json:"input_mode"`
|
| 96 |
+
ThoughtIncluded bool `json:"thought_included"`
|
| 97 |
+
HistorySerialization historySerializationProvenance `json:"history_serialization"`
|
| 98 |
+
Evaluation evaluationProvenance `json:"evaluation"`
|
| 99 |
+
MaxLength int `json:"max_length"`
|
| 100 |
+
FieldMarkers map[string]string `json:"field_markers"`
|
| 101 |
+
FieldBudgets map[string]int `json:"field_budgets"`
|
| 102 |
+
CalibrationScale float64 `json:"calibration_scale"`
|
| 103 |
+
CalibrationBias float64 `json:"calibration_bias"`
|
| 104 |
+
InitialThreshold float64 `json:"initial_threshold"`
|
| 105 |
+
ImportedAt time.Time `json:"imported_at"`
|
| 106 |
+
Artifacts []artifactSpec `json:"artifacts"`
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
type historySerializationProvenance struct {
|
| 110 |
+
EventFields []string `json:"event_fields"`
|
| 111 |
+
EmptyHistory string `json:"empty_history"`
|
| 112 |
+
Observations string `json:"observations"`
|
| 113 |
+
JSONEncoding string `json:"json_encoding"`
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
type evaluationProvenance struct {
|
| 117 |
+
Threshold float64 `json:"threshold"`
|
| 118 |
+
Accuracy float64 `json:"accuracy"`
|
| 119 |
+
Precision float64 `json:"precision"`
|
| 120 |
+
Recall float64 `json:"recall"`
|
| 121 |
+
F1 float64 `json:"f1"`
|
| 122 |
+
MCC float64 `json:"mcc"`
|
| 123 |
+
WorstSourceRecall float64 `json:"worst_source_recall"`
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
func DefaultModelDir(dbPath string) string {
|
| 127 |
+
root := filepath.Join(filepath.Dir(dbPath), "judge-models")
|
| 128 |
+
return filepath.Join(root, "toolsafe", legacyModelVersion)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
func ValidateModelDir(dir string) error {
|
| 132 |
+
if strings.TrimSpace(dir) == "" {
|
| 133 |
+
return backendError(ErrorUnavailable, errors.New("step-safety model directory is empty"))
|
| 134 |
+
}
|
| 135 |
+
for _, artifact := range requiredArtifacts {
|
| 136 |
+
path := filepath.Join(dir, artifact.Name)
|
| 137 |
+
info, err := os.Stat(path)
|
| 138 |
+
if err != nil {
|
| 139 |
+
return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s is unavailable", artifact.Name))
|
| 140 |
+
}
|
| 141 |
+
if !info.Mode().IsRegular() || info.Size() != artifact.Size {
|
| 142 |
+
return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s has unexpected size", artifact.Name))
|
| 143 |
+
}
|
| 144 |
+
digest, err := fileSHA256(path)
|
| 145 |
+
if err != nil || digest != artifact.SHA256 {
|
| 146 |
+
return backendError(ErrorUnavailable, fmt.Errorf("model artifact %s failed checksum verification", artifact.Name))
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
return nil
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// InstallModel copies only inference artifacts into Kontext's existing
|
| 153 |
+
// database-adjacent model cache. It never imports the 849 MB training resume
|
| 154 |
+
// checkpoint and leaves no reference to the source tree.
|
| 155 |
+
func InstallModel(sourceDir, destinationDir string) (string, error) {
|
| 156 |
+
if err := ValidateModelDir(sourceDir); err != nil {
|
| 157 |
+
return "", err
|
| 158 |
+
}
|
| 159 |
+
if err := ValidateModelDir(destinationDir); err == nil {
|
| 160 |
+
return destinationDir, nil
|
| 161 |
+
} else if _, statErr := os.Stat(destinationDir); statErr == nil {
|
| 162 |
+
return "", fmt.Errorf("step-safety destination already exists but is invalid: %s", destinationDir)
|
| 163 |
+
} else if !errors.Is(statErr, os.ErrNotExist) {
|
| 164 |
+
return "", statErr
|
| 165 |
+
}
|
| 166 |
+
parent := filepath.Dir(destinationDir)
|
| 167 |
+
if err := os.MkdirAll(parent, 0o700); err != nil {
|
| 168 |
+
return "", err
|
| 169 |
+
}
|
| 170 |
+
temporary, err := os.MkdirTemp(parent, ".step-safety-install-*")
|
| 171 |
+
if err != nil {
|
| 172 |
+
return "", err
|
| 173 |
+
}
|
| 174 |
+
defer os.RemoveAll(temporary)
|
| 175 |
+
|
| 176 |
+
for _, artifact := range requiredArtifacts {
|
| 177 |
+
if err := copyArtifact(filepath.Join(sourceDir, artifact.Name), filepath.Join(temporary, artifact.Name)); err != nil {
|
| 178 |
+
return "", err
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
if err := os.WriteFile(filepath.Join(temporary, "LICENSE"), []byte(microsoftMITLicense), 0o600); err != nil {
|
| 182 |
+
return "", err
|
| 183 |
+
}
|
| 184 |
+
provenance := Provenance{
|
| 185 |
+
RuntimeFormat: "ONNX opset 17, float32, [1,512] int64 input_ids/attention_mask, [1,2] logits",
|
| 186 |
+
InputPolicy: "scoped-v2: exclude file tools; whole request/action/schema or abstain; filtered history uses first 72 and last 72 tokens, matching training",
|
| 187 |
+
SourceWeightsSHA256: "075d68f8bb4c1b4b98e2d2b2c0d5b013056b202172b080b34e7364b698844c86",
|
| 188 |
+
ExportToolchain: "torch 2.13.0; transformers 5.14.1; onnx 1.22.0; legacy exporter; scripts/step-safety/export_onnx.py",
|
| 189 |
+
EvaluationScope: "Original held-out TS-Bench checkpoint evaluation; not an estimate for scoped-v2 production coverage",
|
| 190 |
+
|
| 191 |
+
SchemaVersion: 3,
|
| 192 |
+
ModelVersion: legacyModelVersion,
|
| 193 |
+
SourceProject: "kontext-security/toolsafe-lab",
|
| 194 |
+
SourceRevision: sourceRevision,
|
| 195 |
+
SourceArtifactPath: sourceArtifactPath,
|
| 196 |
+
SourceResultsPath: sourceResultsPath,
|
| 197 |
+
SourceResultsSHA256: sourceResultsSHA256,
|
| 198 |
+
SourceFindingsPath: sourceFindingsPath,
|
| 199 |
+
SourceFindingsSHA256: sourceFindingsSHA256,
|
| 200 |
+
SourceSerializerPath: sourceSerializerPath,
|
| 201 |
+
SourceSerializerSHA: sourceSerializerSHA,
|
| 202 |
+
SourcePackingPath: sourcePackingPath,
|
| 203 |
+
SourcePackingSHA: sourcePackingSHA,
|
| 204 |
+
SourceProtocolPath: sourceProtocolPath,
|
| 205 |
+
SourceProtocolSHA: sourceProtocolSHA,
|
| 206 |
+
BaseModel: "microsoft/deberta-v3-xsmall",
|
| 207 |
+
BaseModelRevision: "4b419818330868dff6a60ad3e6b1c730f8b8c0c6",
|
| 208 |
+
BaseModelLicense: "MIT",
|
| 209 |
+
InputMode: "execution_context + canonical structured history (agent Thought excluded)",
|
| 210 |
+
ThoughtIncluded: false,
|
| 211 |
+
HistorySerialization: historySerializationProvenance{
|
| 212 |
+
EventFields: []string{"tool", "arguments", "observation"},
|
| 213 |
+
EmptyHistory: "[]",
|
| 214 |
+
Observations: "strings",
|
| 215 |
+
JSONEncoding: "UTF-8, sorted keys, compact separators",
|
| 216 |
+
},
|
| 217 |
+
Evaluation: evaluationProvenance{
|
| 218 |
+
Threshold: 0.5,
|
| 219 |
+
Accuracy: 0.9118629908103593,
|
| 220 |
+
Precision: 0.9266195885784464,
|
| 221 |
+
Recall: 0.8845252051582649,
|
| 222 |
+
F1: 0.9050832208726945,
|
| 223 |
+
MCC: 0.8236797425872459,
|
| 224 |
+
WorstSourceRecall: 0.5085227272727273,
|
| 225 |
+
},
|
| 226 |
+
MaxLength: 512,
|
| 227 |
+
FieldMarkers: map[string]string{
|
| 228 |
+
"request": "[USER_REQUEST]",
|
| 229 |
+
"history": "[INTERACTION_HISTORY]",
|
| 230 |
+
"action": "[CURRENT_ACTION]",
|
| 231 |
+
"schema": "[TOOL_DESCRIPTIONS]",
|
| 232 |
+
},
|
| 233 |
+
FieldBudgets: map[string]int{
|
| 234 |
+
"request": 96,
|
| 235 |
+
"history": 144,
|
| 236 |
+
"action": 128,
|
| 237 |
+
"schema": 128,
|
| 238 |
+
},
|
| 239 |
+
CalibrationScale: 1.427213430140093,
|
| 240 |
+
CalibrationBias: 2.953687013257505,
|
| 241 |
+
InitialThreshold: 0.5,
|
| 242 |
+
ImportedAt: time.Now().UTC(),
|
| 243 |
+
Artifacts: append([]artifactSpec(nil), requiredArtifacts...),
|
| 244 |
+
}
|
| 245 |
+
encoded, err := json.MarshalIndent(provenance, "", " ")
|
| 246 |
+
if err != nil {
|
| 247 |
+
return "", err
|
| 248 |
+
}
|
| 249 |
+
if err := os.WriteFile(filepath.Join(temporary, "PROVENANCE.json"), append(encoded, '\n'), 0o600); err != nil {
|
| 250 |
+
return "", err
|
| 251 |
+
}
|
| 252 |
+
if err := ValidateModelDir(temporary); err != nil {
|
| 253 |
+
return "", err
|
| 254 |
+
}
|
| 255 |
+
if err := os.Rename(temporary, destinationDir); err != nil {
|
| 256 |
+
return "", err
|
| 257 |
+
}
|
| 258 |
+
return destinationDir, nil
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
func RequiredArtifacts() []string {
|
| 262 |
+
names := make([]string, 0, len(requiredArtifacts))
|
| 263 |
+
for _, artifact := range requiredArtifacts {
|
| 264 |
+
names = append(names, artifact.Name)
|
| 265 |
+
}
|
| 266 |
+
sort.Strings(names)
|
| 267 |
+
return names
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
func copyArtifact(source, destination string) error {
|
| 271 |
+
in, err := os.Open(source)
|
| 272 |
+
if err != nil {
|
| 273 |
+
return err
|
| 274 |
+
}
|
| 275 |
+
defer in.Close()
|
| 276 |
+
out, err := os.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
| 277 |
+
if err != nil {
|
| 278 |
+
return err
|
| 279 |
+
}
|
| 280 |
+
_, copyErr := io.Copy(out, in)
|
| 281 |
+
closeErr := out.Close()
|
| 282 |
+
return errors.Join(copyErr, closeErr)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
func fileSHA256(path string) (string, error) {
|
| 286 |
+
file, err := os.Open(path)
|
| 287 |
+
if err != nil {
|
| 288 |
+
return "", err
|
| 289 |
+
}
|
| 290 |
+
defer file.Close()
|
| 291 |
+
digest := sha256.New()
|
| 292 |
+
if _, err := io.Copy(digest, file); err != nil {
|
| 293 |
+
return "", err
|
| 294 |
+
}
|
| 295 |
+
return hex.EncodeToString(digest.Sum(nil)), nil
|
| 296 |
+
}
|
v3-ungated/native/stepsafety/native_assets.go
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build !nostepsafetymodel
|
| 2 |
+
|
| 3 |
+
package stepsafety
|
| 4 |
+
|
| 5 |
+
import "embed"
|
| 6 |
+
|
| 7 |
+
// nativeAssets holds the checkpoint, tokenizer and classifiers. They are about
|
| 8 |
+
// 213 MB of the binary; the nostepsafetymodel tag leaves them out.
|
| 9 |
+
//
|
| 10 |
+
//go:embed model/native/*.gz
|
| 11 |
+
var nativeAssets embed.FS
|
| 12 |
+
|
| 13 |
+
// modelEmbedded reports whether this build carries the model; tests that
|
| 14 |
+
// need it skip without it.
|
| 15 |
+
const modelEmbedded = true
|
v3-ungated/native/stepsafety/native_assets_nomodel.go
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build nostepsafetymodel
|
| 2 |
+
|
| 3 |
+
package stepsafety
|
| 4 |
+
|
| 5 |
+
import "embed"
|
| 6 |
+
|
| 7 |
+
// nativeAssets is empty in a build without the model, for hosts that cannot
|
| 8 |
+
// carry the full binary. Loading then fails, so New returns an unavailable
|
| 9 |
+
// evaluator and health reports "unavailable"; decisions do not depend on it.
|
| 10 |
+
var nativeAssets embed.FS
|
| 11 |
+
|
| 12 |
+
// modelEmbedded reports whether this build carries the model; tests that
|
| 13 |
+
// need it skip without it.
|
| 14 |
+
const modelEmbedded = false
|
v3-ungated/native/stepsafety/native_backend.go
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"compress/gzip"
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/sha256"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"errors"
|
| 9 |
+
"io"
|
| 10 |
+
"io/fs"
|
| 11 |
+
"sync"
|
| 12 |
+
"sync/atomic"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
var nativeOnce sync.Once
|
| 16 |
+
var nativeReady = make(chan struct{})
|
| 17 |
+
var nativeShared struct {
|
| 18 |
+
model *nativeModel
|
| 19 |
+
tokenizer *tokenizer
|
| 20 |
+
err error
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
type nativeBackend struct {
|
| 24 |
+
model *nativeModel
|
| 25 |
+
tokenizer *tokenizer
|
| 26 |
+
modelVersion string
|
| 27 |
+
closed atomic.Bool
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Load one immutable, checksum-verified checkpoint per process, just as Kestrel
|
| 31 |
+
// does. No files are extracted, no library is loaded, and no download occurs.
|
| 32 |
+
// A caller's startup deadline never poisons the shared load for later callers.
|
| 33 |
+
func newNativeBackend(ctx context.Context, cfg Config) (*nativeBackend, error) {
|
| 34 |
+
ctx, cancel := context.WithTimeout(ctx, cfg.StartupTimeout)
|
| 35 |
+
defer cancel()
|
| 36 |
+
nativeOnce.Do(func() {
|
| 37 |
+
go func() {
|
| 38 |
+
defer close(nativeReady)
|
| 39 |
+
nativeShared.model, nativeShared.tokenizer, nativeShared.err = loadNativeAssets(context.Background())
|
| 40 |
+
if nativeShared.err == nil {
|
| 41 |
+
nativeShared.err = loadAgreementAssets()
|
| 42 |
+
if nativeShared.err == nil {
|
| 43 |
+
nativeShared.err = loadRequestGateAssets()
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}()
|
| 47 |
+
})
|
| 48 |
+
select {
|
| 49 |
+
case <-ctx.Done():
|
| 50 |
+
return nil, ctx.Err()
|
| 51 |
+
case <-nativeReady:
|
| 52 |
+
if nativeShared.err != nil {
|
| 53 |
+
return nil, backendError(ErrorUnavailable, nativeShared.err)
|
| 54 |
+
}
|
| 55 |
+
return &nativeBackend{model: nativeShared.model, tokenizer: nativeShared.tokenizer, modelVersion: cfg.ModelVersion}, nil
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func loadNativeAssets(ctx context.Context) (*nativeModel, *tokenizer, error) {
|
| 60 |
+
names, err := fs.Glob(nativeAssets, "model/native/weights.*.gz")
|
| 61 |
+
if err != nil || len(names) != 5 {
|
| 62 |
+
return nil, nil, errors.New("missing embedded native weights")
|
| 63 |
+
}
|
| 64 |
+
readers := make([]io.Reader, 0, len(names))
|
| 65 |
+
for _, name := range names {
|
| 66 |
+
f, err := nativeAssets.Open(name)
|
| 67 |
+
if err != nil {
|
| 68 |
+
return nil, nil, err
|
| 69 |
+
}
|
| 70 |
+
defer f.Close()
|
| 71 |
+
readers = append(readers, f)
|
| 72 |
+
}
|
| 73 |
+
r, err := gzip.NewReader(io.MultiReader(readers...))
|
| 74 |
+
if err != nil {
|
| 75 |
+
return nil, nil, err
|
| 76 |
+
}
|
| 77 |
+
defer r.Close()
|
| 78 |
+
weights, err := readNativeWeights(ctx, r)
|
| 79 |
+
if err != nil {
|
| 80 |
+
return nil, nil, err
|
| 81 |
+
}
|
| 82 |
+
model, err := buildNativeModel(ctx, weights)
|
| 83 |
+
if err != nil {
|
| 84 |
+
return nil, nil, err
|
| 85 |
+
}
|
| 86 |
+
f, err := nativeAssets.Open("model/native/tokenizer.json.gz")
|
| 87 |
+
if err != nil {
|
| 88 |
+
return nil, nil, err
|
| 89 |
+
}
|
| 90 |
+
defer f.Close()
|
| 91 |
+
t, err := gzip.NewReader(f)
|
| 92 |
+
if err != nil {
|
| 93 |
+
return nil, nil, err
|
| 94 |
+
}
|
| 95 |
+
defer t.Close()
|
| 96 |
+
data, err := io.ReadAll(io.LimitReader(t, 8340655))
|
| 97 |
+
if err != nil {
|
| 98 |
+
return nil, nil, err
|
| 99 |
+
}
|
| 100 |
+
digest := sha256.Sum256(data)
|
| 101 |
+
if len(data) != 8340654 || hex.EncodeToString(digest[:]) != "d6c20af053b5d86d986a9f70898c1fceccb9d93e7ce6f63dabc899a12a53b031" {
|
| 102 |
+
return nil, nil, errors.New("embedded tokenizer checksum mismatch")
|
| 103 |
+
}
|
| 104 |
+
tok, err := parseTokenizer(data)
|
| 105 |
+
return model, tok, err
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
func (b *nativeBackend) Infer(ctx context.Context, input Input) (InferenceResult, error) {
|
| 109 |
+
if b.closed.Load() {
|
| 110 |
+
return InferenceResult{}, backendError(ErrorUnavailable, nil)
|
| 111 |
+
}
|
| 112 |
+
packed, err := packScopedInput(ctx, b.tokenizer, input)
|
| 113 |
+
if err != nil {
|
| 114 |
+
return InferenceResult{}, err
|
| 115 |
+
}
|
| 116 |
+
out, err := b.model.infer(ctx, packed)
|
| 117 |
+
if err != nil {
|
| 118 |
+
return out, err
|
| 119 |
+
}
|
| 120 |
+
return applyAgreement(ctx, input, out)
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func (b *nativeBackend) Health(ctx context.Context) (Health, error) {
|
| 124 |
+
if err := ctx.Err(); err != nil {
|
| 125 |
+
return Health{}, err
|
| 126 |
+
}
|
| 127 |
+
if b.closed.Load() {
|
| 128 |
+
return Health{}, backendError(ErrorUnavailable, nil)
|
| 129 |
+
}
|
| 130 |
+
return Health{Status: "ready", ModelVersion: b.modelVersion, Device: "go-cpu"}, nil
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
func (b *nativeBackend) Close() error { b.closed.Store(true); return nil }
|
v3-ungated/native/stepsafety/native_linear_arm64.go
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build arm64 && !purego
|
| 2 |
+
|
| 3 |
+
package stepsafety
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"runtime"
|
| 7 |
+
"sync"
|
| 8 |
+
|
| 9 |
+
"gonum.org/v1/gonum/blas"
|
| 10 |
+
"gonum.org/v1/gonum/blas/gonum"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// Pack each group of eight output columns together. The NEON kernel consumes
|
| 14 |
+
// four rows and eight columns without a C ABI, dynamic library, or cgo.
|
| 15 |
+
func prepareNativeLinear(l nativeLinear) nativeLinear {
|
| 16 |
+
return prepareNativeLinearInto(l, nil)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
func prepareNativeLinearInto(l nativeLinear, scratch []float32) nativeLinear {
|
| 20 |
+
if l.out < 8 || len(l.weight) != l.in*l.out {
|
| 21 |
+
return l
|
| 22 |
+
}
|
| 23 |
+
l.packed = nativeBuffer(scratch, (l.out+7)/8*8*l.in)
|
| 24 |
+
for j := 0; j < l.out; j += 8 {
|
| 25 |
+
for k := 0; k < l.in; k++ {
|
| 26 |
+
for c := 0; c < 8; c++ {
|
| 27 |
+
if j+c < l.out {
|
| 28 |
+
l.packed[j*l.in+k*8+c] = l.weight[(j+c)*l.in+k]
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
l.weight = nil
|
| 34 |
+
return l
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func nativeLinearProduct(l nativeLinear, x []float32, rows int) []float32 {
|
| 38 |
+
return nativeLinearProductInto(l, x, rows, nil)
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func nativeLinearProductInto(l nativeLinear, x []float32, rows int, scratch []float32) []float32 {
|
| 42 |
+
if len(l.packed) == 0 {
|
| 43 |
+
out := nativeBuffer(scratch, rows*l.out)
|
| 44 |
+
gonum.Implementation{}.Sgemm(blas.NoTrans, blas.Trans, rows, l.out, l.in, 1, x, l.in, l.weight, l.in, 0, out, l.out)
|
| 45 |
+
return out
|
| 46 |
+
}
|
| 47 |
+
padded := (rows + 3) / 4 * 4
|
| 48 |
+
if rows != padded {
|
| 49 |
+
if cap(x) >= padded*l.in {
|
| 50 |
+
x = x[:padded*l.in]
|
| 51 |
+
} else {
|
| 52 |
+
temp := make([]float32, padded*l.in)
|
| 53 |
+
copy(temp, x)
|
| 54 |
+
x = temp
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
stride := (l.out + 7) / 8 * 8
|
| 58 |
+
out := nativeBuffer(scratch, padded*stride)
|
| 59 |
+
workers := min(4, runtime.GOMAXPROCS(0), padded/4)
|
| 60 |
+
var wg sync.WaitGroup
|
| 61 |
+
for worker := 0; worker < workers; worker++ {
|
| 62 |
+
wg.Add(1)
|
| 63 |
+
go func(worker int) {
|
| 64 |
+
defer wg.Done()
|
| 65 |
+
for i := worker * 4; i < padded; i += workers * 4 {
|
| 66 |
+
for j := 0; j < l.out; j += 8 {
|
| 67 |
+
nativeMatmul4x8(&x[i*l.in], &l.packed[j*l.in], &out[i*stride+j], l.in, stride)
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
}(worker)
|
| 71 |
+
}
|
| 72 |
+
wg.Wait()
|
| 73 |
+
if stride != l.out {
|
| 74 |
+
for i := 0; i < rows; i++ {
|
| 75 |
+
copy(out[i*l.out:(i+1)*l.out], out[i*stride:i*stride+l.out])
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
return out[:rows*l.out]
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
//go:noescape
|
| 82 |
+
func nativeMatmul4x8(a, packedB, c *float32, k, strideC int)
|
v3-ungated/native/stepsafety/native_linear_arm64.s
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build arm64 && !purego
|
| 2 |
+
|
| 3 |
+
#include "textflag.h"
|
| 4 |
+
|
| 5 |
+
// C[4,8] = A[4,k] * B[k,8]. All dimensions checked by the Go caller.
|
| 6 |
+
// B is a contiguous packed panel; C rows have strideC elements.
|
| 7 |
+
TEXT ·nativeMatmul4x8(SB), NOSPLIT, $0-40
|
| 8 |
+
MOVD a+0(FP), R0
|
| 9 |
+
MOVD packedB+8(FP), R1
|
| 10 |
+
MOVD c+16(FP), R2
|
| 11 |
+
MOVD k+24(FP), R3
|
| 12 |
+
MOVD strideC+32(FP), R4
|
| 13 |
+
LSL $2, R3, R5
|
| 14 |
+
LSL $2, R4, R4
|
| 15 |
+
ADD R5, R0, R6
|
| 16 |
+
ADD R5, R6, R7
|
| 17 |
+
ADD R5, R7, R8
|
| 18 |
+
VEOR V16.B16, V16.B16, V16.B16
|
| 19 |
+
VEOR V17.B16, V17.B16, V17.B16
|
| 20 |
+
VEOR V18.B16, V18.B16, V18.B16
|
| 21 |
+
VEOR V19.B16, V19.B16, V19.B16
|
| 22 |
+
VEOR V20.B16, V20.B16, V20.B16
|
| 23 |
+
VEOR V21.B16, V21.B16, V21.B16
|
| 24 |
+
VEOR V22.B16, V22.B16, V22.B16
|
| 25 |
+
VEOR V23.B16, V23.B16, V23.B16
|
| 26 |
+
loop:
|
| 27 |
+
FMOVS (R0), F0
|
| 28 |
+
FMOVS (R6), F1
|
| 29 |
+
FMOVS (R7), F2
|
| 30 |
+
FMOVS (R8), F3
|
| 31 |
+
VDUP V0.S[0], V0.S4
|
| 32 |
+
VDUP V1.S[0], V1.S4
|
| 33 |
+
VDUP V2.S[0], V2.S4
|
| 34 |
+
VDUP V3.S[0], V3.S4
|
| 35 |
+
VLD1.P 32(R1), [V4.S4, V5.S4]
|
| 36 |
+
VFMLA V4.S4, V0.S4, V16.S4
|
| 37 |
+
VFMLA V5.S4, V0.S4, V17.S4
|
| 38 |
+
VFMLA V4.S4, V1.S4, V18.S4
|
| 39 |
+
VFMLA V5.S4, V1.S4, V19.S4
|
| 40 |
+
VFMLA V4.S4, V2.S4, V20.S4
|
| 41 |
+
VFMLA V5.S4, V2.S4, V21.S4
|
| 42 |
+
VFMLA V4.S4, V3.S4, V22.S4
|
| 43 |
+
VFMLA V5.S4, V3.S4, V23.S4
|
| 44 |
+
ADD $4, R0, R0
|
| 45 |
+
ADD $4, R6, R6
|
| 46 |
+
ADD $4, R7, R7
|
| 47 |
+
ADD $4, R8, R8
|
| 48 |
+
SUBS $1, R3, R3
|
| 49 |
+
BNE loop
|
| 50 |
+
VST1 [V16.S4, V17.S4], (R2)
|
| 51 |
+
ADD R4, R2, R2
|
| 52 |
+
VST1 [V18.S4, V19.S4], (R2)
|
| 53 |
+
ADD R4, R2, R2
|
| 54 |
+
VST1 [V20.S4, V21.S4], (R2)
|
| 55 |
+
ADD R4, R2, R2
|
| 56 |
+
VST1 [V22.S4, V23.S4], (R2)
|
| 57 |
+
RET
|
v3-ungated/native/stepsafety/native_linear_generic.go
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:build !arm64 || purego
|
| 2 |
+
|
| 3 |
+
package stepsafety
|
| 4 |
+
|
| 5 |
+
import (
|
| 6 |
+
"gonum.org/v1/gonum/blas"
|
| 7 |
+
"gonum.org/v1/gonum/blas/gonum"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func prepareNativeLinear(l nativeLinear) nativeLinear { return l }
|
| 11 |
+
func prepareNativeLinearInto(l nativeLinear, _ []float32) nativeLinear { return l }
|
| 12 |
+
|
| 13 |
+
func nativeLinearProduct(l nativeLinear, x []float32, rows int) []float32 {
|
| 14 |
+
return nativeLinearProductInto(l, x, rows, nil)
|
| 15 |
+
}
|
| 16 |
+
func nativeLinearProductInto(l nativeLinear, x []float32, rows int, scratch []float32) []float32 {
|
| 17 |
+
out := nativeBuffer(scratch, rows*l.out)
|
| 18 |
+
gonum.Implementation{}.Sgemm(blas.NoTrans, blas.Trans, rows, l.out, l.in, 1, x, l.in, l.weight, l.in, 0, out, l.out)
|
| 19 |
+
return out
|
| 20 |
+
}
|
v3-ungated/native/stepsafety/native_model.go
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"errors"
|
| 6 |
+
"fmt"
|
| 7 |
+
"math"
|
| 8 |
+
"sync"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
const (
|
| 12 |
+
nativeHidden = 384
|
| 13 |
+
nativeHeads = 6
|
| 14 |
+
nativeHeadSize = 64
|
| 15 |
+
nativeIntermediate = 1536
|
| 16 |
+
nativeLayers = 12
|
| 17 |
+
nativePositions = 512
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
// This is the pinned DeBERTa-v3-xsmall evaluation graph, not a general-purpose
|
| 21 |
+
// transformer interpreter. Dropout is disabled; there are no token-type,
|
| 22 |
+
// absolute-position, convolution, or decoder layers in this checkpoint.
|
| 23 |
+
type nativeModel struct {
|
| 24 |
+
embeddings []float32
|
| 25 |
+
embeddingNorm nativeNorm
|
| 26 |
+
layers [nativeLayers]nativeLayer
|
| 27 |
+
pooler, classifier nativeLinear
|
| 28 |
+
work sync.Pool
|
| 29 |
+
}
|
| 30 |
+
type nativeNorm struct{ weight, bias []float32 }
|
| 31 |
+
type nativeLinear struct {
|
| 32 |
+
weight, bias []float32
|
| 33 |
+
packed []float32
|
| 34 |
+
in, out int
|
| 35 |
+
}
|
| 36 |
+
type nativeLayer struct {
|
| 37 |
+
query, key, value, attention, intermediate, output nativeLinear
|
| 38 |
+
attentionNorm, outputNorm nativeNorm
|
| 39 |
+
relative [nativeHeads]struct{ query, key nativeLinear }
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func buildNativeModel(ctx context.Context, tensors map[string]nativeTensor) (*nativeModel, error) {
|
| 43 |
+
m := &nativeModel{}
|
| 44 |
+
var firstErr error
|
| 45 |
+
weight := func(name string, shape ...int) []float32 {
|
| 46 |
+
data, err := nativeWeight(tensors, name, shape...)
|
| 47 |
+
if firstErr == nil {
|
| 48 |
+
firstErr = err
|
| 49 |
+
}
|
| 50 |
+
return data
|
| 51 |
+
}
|
| 52 |
+
norm := func(name string) nativeNorm {
|
| 53 |
+
return nativeNorm{weight(name+".weight", nativeHidden), weight(name+".bias", nativeHidden)}
|
| 54 |
+
}
|
| 55 |
+
linear := func(name string, in, out int) nativeLinear {
|
| 56 |
+
return prepareNativeLinear(nativeLinear{weight: weight(name+".weight", out, in), bias: weight(name+".bias", out), in: in, out: out})
|
| 57 |
+
}
|
| 58 |
+
m.embeddings = weight("deberta.embeddings.word_embeddings.weight", 128005, nativeHidden)
|
| 59 |
+
m.embeddingNorm = norm("deberta.embeddings.LayerNorm")
|
| 60 |
+
rel := weight("deberta.encoder.rel_embeddings.weight", nativePositions, nativeHidden)
|
| 61 |
+
relNorm := norm("deberta.encoder.LayerNorm")
|
| 62 |
+
for i := range m.layers {
|
| 63 |
+
p := fmt.Sprintf("deberta.encoder.layer.%d.", i)
|
| 64 |
+
l := &m.layers[i]
|
| 65 |
+
l.query = linear(p+"attention.self.query_proj", nativeHidden, nativeHidden)
|
| 66 |
+
l.key = linear(p+"attention.self.key_proj", nativeHidden, nativeHidden)
|
| 67 |
+
l.value = linear(p+"attention.self.value_proj", nativeHidden, nativeHidden)
|
| 68 |
+
l.attention = linear(p+"attention.output.dense", nativeHidden, nativeHidden)
|
| 69 |
+
l.attentionNorm = norm(p + "attention.output.LayerNorm")
|
| 70 |
+
l.intermediate = linear(p+"intermediate.dense", nativeHidden, nativeIntermediate)
|
| 71 |
+
l.output = linear(p+"output.dense", nativeIntermediate, nativeHidden)
|
| 72 |
+
l.outputNorm = norm(p + "output.LayerNorm")
|
| 73 |
+
}
|
| 74 |
+
m.pooler = linear("pooler.dense", nativeHidden, nativeHidden)
|
| 75 |
+
m.classifier = linear("classifier", nativeHidden, 2)
|
| 76 |
+
if firstErr != nil {
|
| 77 |
+
return nil, firstErr
|
| 78 |
+
}
|
| 79 |
+
// Relative embeddings and their projections depend only on weights. Cache
|
| 80 |
+
// them once; every inference uses the same exact float32 parameters.
|
| 81 |
+
rel = append([]float32(nil), rel...)
|
| 82 |
+
relNorm.apply(rel, nil)
|
| 83 |
+
for i := range m.layers {
|
| 84 |
+
if err := ctx.Err(); err != nil {
|
| 85 |
+
return nil, err
|
| 86 |
+
}
|
| 87 |
+
l := &m.layers[i]
|
| 88 |
+
relQ, relK := l.query.apply(rel, nativePositions), l.key.apply(rel, nativePositions)
|
| 89 |
+
for h := 0; h < nativeHeads; h++ {
|
| 90 |
+
l.relative[h].query = prepareNativeLinear(nativeLinear{weight: nativeHead(relQ, nativePositions, h), in: nativeHeadSize, out: nativePositions})
|
| 91 |
+
l.relative[h].key = prepareNativeLinear(nativeLinear{weight: nativeHead(relK, nativePositions, h), in: nativeHeadSize, out: nativePositions})
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
return m, nil
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
func (l nativeLinear) apply(x []float32, rows int) []float32 {
|
| 98 |
+
return l.applyInto(x, rows, nil)
|
| 99 |
+
}
|
| 100 |
+
func (l nativeLinear) applyInto(x []float32, rows int, scratch []float32) []float32 {
|
| 101 |
+
out := nativeLinearProductInto(l, x, rows, scratch)
|
| 102 |
+
for i := 0; i < rows; i++ {
|
| 103 |
+
row := out[i*l.out : (i+1)*l.out]
|
| 104 |
+
for j := range row {
|
| 105 |
+
row[j] += l.bias[j]
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
return out
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
func (norm nativeNorm) apply(x, residual []float32) {
|
| 112 |
+
for start := 0; start < len(x); start += nativeHidden {
|
| 113 |
+
row := x[start : start+nativeHidden]
|
| 114 |
+
var mean float64
|
| 115 |
+
for j := range row {
|
| 116 |
+
if residual != nil {
|
| 117 |
+
row[j] += residual[start+j]
|
| 118 |
+
}
|
| 119 |
+
mean += float64(row[j])
|
| 120 |
+
}
|
| 121 |
+
mean /= nativeHidden
|
| 122 |
+
var variance float64
|
| 123 |
+
for _, v := range row {
|
| 124 |
+
d := float64(v) - mean
|
| 125 |
+
variance += d * d
|
| 126 |
+
}
|
| 127 |
+
inv := float32(1 / math.Sqrt(variance/nativeHidden+1e-7))
|
| 128 |
+
mu := float32(mean)
|
| 129 |
+
for j := range row {
|
| 130 |
+
row[j] = (row[j]-mu)*inv*norm.weight[j] + norm.bias[j]
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
func nativeGELU(x []float32) {
|
| 136 |
+
for i, v := range x {
|
| 137 |
+
x[i] = v * 0.5 * (1 + float32(math.Erf(float64(v)/math.Sqrt2)))
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Trimming trailing masked padding is exact for this encoder: valid queries
|
| 142 |
+
// never attend to padding, and all other operations are per-token. Preserve
|
| 143 |
+
// every real token, its original position, and the checkpoint's relative buckets.
|
| 144 |
+
func nativeSequenceLength(packed packedInput) (int, error) {
|
| 145 |
+
if len(packed.IDs) != maxSequenceLength || len(packed.Mask) != maxSequenceLength {
|
| 146 |
+
return 0, errors.New("invalid native input shape")
|
| 147 |
+
}
|
| 148 |
+
n := 0
|
| 149 |
+
for i, mask := range packed.Mask {
|
| 150 |
+
if packed.IDs[i] < 0 || packed.IDs[i] >= 128005 {
|
| 151 |
+
return 0, errors.New("invalid native token")
|
| 152 |
+
}
|
| 153 |
+
if mask == 1 && i == n {
|
| 154 |
+
n++
|
| 155 |
+
} else if mask != 0 {
|
| 156 |
+
return 0, errors.New("non-contiguous native attention mask")
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
if n == 0 {
|
| 160 |
+
return 0, errors.New("empty native input")
|
| 161 |
+
}
|
| 162 |
+
return n, nil
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
func nativeRelativePosition(distance int) int {
|
| 166 |
+
abs := distance
|
| 167 |
+
if abs < 0 {
|
| 168 |
+
abs = -abs
|
| 169 |
+
}
|
| 170 |
+
if abs <= 128 {
|
| 171 |
+
return distance
|
| 172 |
+
}
|
| 173 |
+
// Mirror make_log_bucket_position(bucket_size=256,max_position=512).
|
| 174 |
+
bucket := int(math.Ceil(math.Log(float64(abs)/128)/math.Log(511.0/128)*127)) + 128
|
| 175 |
+
if distance < 0 {
|
| 176 |
+
return -bucket
|
| 177 |
+
}
|
| 178 |
+
return bucket
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
func (m *nativeModel) infer(ctx context.Context, packed packedInput) (InferenceResult, error) {
|
| 182 |
+
n, err := nativeSequenceLength(packed)
|
| 183 |
+
if err != nil {
|
| 184 |
+
return InferenceResult{}, err
|
| 185 |
+
}
|
| 186 |
+
w, ok := m.work.Get().(*nativeWorkspace)
|
| 187 |
+
if !ok {
|
| 188 |
+
w = newNativeWorkspace()
|
| 189 |
+
}
|
| 190 |
+
defer m.work.Put(w)
|
| 191 |
+
x := w.x[:n*nativeHidden]
|
| 192 |
+
for i, id := range packed.IDs[:n] {
|
| 193 |
+
copy(x[i*nativeHidden:], m.embeddings[int(id)*nativeHidden:(int(id)+1)*nativeHidden])
|
| 194 |
+
}
|
| 195 |
+
m.embeddingNorm.apply(x, nil)
|
| 196 |
+
positions := w.positions[:n*n]
|
| 197 |
+
for i := 0; i < n; i++ {
|
| 198 |
+
for j := 0; j < n; j++ {
|
| 199 |
+
positions[i*n+j] = min(max(nativeRelativePosition(i-j)+256, 0), 511)
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
for _, l := range m.layers {
|
| 203 |
+
if err := ctx.Err(); err != nil {
|
| 204 |
+
return InferenceResult{}, err
|
| 205 |
+
}
|
| 206 |
+
q, k, v := l.query.applyInto(x, n, w.q), l.key.applyInto(x, n, w.k), l.value.applyInto(x, n, w.v)
|
| 207 |
+
attention := w.attention[:n*nativeHidden]
|
| 208 |
+
var wg sync.WaitGroup
|
| 209 |
+
for head := 0; head < nativeHeads; head++ {
|
| 210 |
+
wg.Add(1)
|
| 211 |
+
go func(head int) {
|
| 212 |
+
defer wg.Done()
|
| 213 |
+
nativeAttention(ctx, q, k, v, l.relative[head].query, l.relative[head].key, positions, attention, n, head, &w.heads[head])
|
| 214 |
+
}(head)
|
| 215 |
+
}
|
| 216 |
+
wg.Wait()
|
| 217 |
+
if err := ctx.Err(); err != nil {
|
| 218 |
+
return InferenceResult{}, err
|
| 219 |
+
}
|
| 220 |
+
a := l.attention.applyInto(attention, n, w.a)
|
| 221 |
+
l.attentionNorm.apply(a, x)
|
| 222 |
+
intermediate := l.intermediate.applyInto(a, n, w.intermediate)
|
| 223 |
+
nativeGELU(intermediate)
|
| 224 |
+
x = l.output.applyInto(intermediate, n, w.x)
|
| 225 |
+
l.outputNorm.apply(x, a)
|
| 226 |
+
}
|
| 227 |
+
if err := ctx.Err(); err != nil {
|
| 228 |
+
return InferenceResult{}, err
|
| 229 |
+
}
|
| 230 |
+
pooled := m.pooler.apply(x[:nativeHidden], 1)
|
| 231 |
+
nativeGELU(pooled)
|
| 232 |
+
logits := m.classifier.apply(pooled, 1)
|
| 233 |
+
return InferenceResult{Logits: [2]float64{float64(logits[0]), float64(logits[1])}, HistoryOmitted: packed.HistoryOmitted}, nil
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
func nativeHead(x []float32, n, head int) []float32 {
|
| 237 |
+
return nativeHeadInto(x, n, head, nil)
|
| 238 |
+
}
|
| 239 |
+
func nativeHeadInto(x []float32, n, head int, scratch []float32) []float32 {
|
| 240 |
+
out := nativeBuffer(scratch, n*nativeHeadSize)
|
| 241 |
+
for i := 0; i < n; i++ {
|
| 242 |
+
copy(out[i*nativeHeadSize:], x[i*nativeHidden+head*nativeHeadSize:i*nativeHidden+(head+1)*nativeHeadSize])
|
| 243 |
+
}
|
| 244 |
+
return out
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
func nativeAttention(ctx context.Context, q, k, v []float32, relQ, relK nativeLinear, positions []int, output []float32, n, head int, w *nativeHeadWorkspace) {
|
| 248 |
+
scale := float32(math.Sqrt(nativeHeadSize * 3))
|
| 249 |
+
qh, kh, vh := nativeHeadInto(q, n, head, w.q), nativeHeadInto(k, n, head, w.k), nativeHeadInto(v, n, head, w.v)
|
| 250 |
+
scores := nativeLinearProductInto(prepareNativeLinearInto(nativeLinear{weight: kh, in: nativeHeadSize, out: n}, w.packedK), qh, n, w.scores)
|
| 251 |
+
c2p := nativeLinearProductInto(relK, qh, n, w.c2p)
|
| 252 |
+
p2c := nativeLinearProductInto(relQ, kh, n, w.p2c)
|
| 253 |
+
offset := head * nativeHeadSize
|
| 254 |
+
for i := 0; i < n; i++ {
|
| 255 |
+
if ctx.Err() != nil {
|
| 256 |
+
return
|
| 257 |
+
}
|
| 258 |
+
row := scores[i*n : (i+1)*n]
|
| 259 |
+
largest := float32(-math.MaxFloat32)
|
| 260 |
+
for j := range row {
|
| 261 |
+
pos := positions[i*n+j]
|
| 262 |
+
// p2c gathers (-relative_pos), then transposes the query/key axes;
|
| 263 |
+
// for equal-length self attention this selects the same i-j bucket.
|
| 264 |
+
row[j] = row[j]/scale + (c2p[i*nativePositions+pos]/scale + p2c[j*nativePositions+pos]/scale)
|
| 265 |
+
if row[j] > largest {
|
| 266 |
+
largest = row[j]
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
var sum float32
|
| 270 |
+
for j := range row {
|
| 271 |
+
row[j] = float32(math.Exp(float64(row[j] - largest)))
|
| 272 |
+
sum += row[j]
|
| 273 |
+
}
|
| 274 |
+
for j := range row {
|
| 275 |
+
row[j] /= sum
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
vt := w.vt[:n*nativeHeadSize]
|
| 279 |
+
for i := 0; i < n; i++ {
|
| 280 |
+
for j := 0; j < nativeHeadSize; j++ {
|
| 281 |
+
vt[j*n+i] = vh[i*nativeHeadSize+j]
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
result := nativeLinearProductInto(prepareNativeLinearInto(nativeLinear{weight: vt, in: n, out: nativeHeadSize}, w.packedV), scores, n, w.result)
|
| 285 |
+
for i := 0; i < n; i++ {
|
| 286 |
+
copy(output[i*nativeHidden+offset:], result[i*nativeHeadSize:(i+1)*nativeHeadSize])
|
| 287 |
+
}
|
| 288 |
+
}
|
v3-ungated/native/stepsafety/native_weights.go
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"crypto/sha256"
|
| 6 |
+
"encoding/binary"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"errors"
|
| 10 |
+
"fmt"
|
| 11 |
+
"io"
|
| 12 |
+
"math"
|
| 13 |
+
"slices"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// The native backend consumes the original float32 checkpoint, without
|
| 17 |
+
// quantization, conversion through ONNX, or any change to its learned weights.
|
| 18 |
+
const nativeWeightsSHA256 = "429b09164c6705790c4414eb31c0bc18d2fa8a374bea467b2e1b49ead6aeb5f1"
|
| 19 |
+
const nativeWeightsBytes = 283201512
|
| 20 |
+
|
| 21 |
+
type nativeTensor struct {
|
| 22 |
+
Shape []int
|
| 23 |
+
Data []float32
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// readNativeWeights supports only the pinned checkpoint's F32 safetensors
|
| 27 |
+
// subset. Validate offsets and lengths before allocating, and verify the entire
|
| 28 |
+
// byte stream against the training pin before exposing any tensor to inference.
|
| 29 |
+
func readNativeWeights(ctx context.Context, source io.Reader) (map[string]nativeTensor, error) {
|
| 30 |
+
digest := sha256.New()
|
| 31 |
+
r := io.TeeReader(io.LimitReader(source, nativeWeightsBytes+1), digest)
|
| 32 |
+
var headerSize uint64
|
| 33 |
+
if err := binary.Read(r, binary.LittleEndian, &headerSize); err != nil {
|
| 34 |
+
return nil, err
|
| 35 |
+
}
|
| 36 |
+
if headerSize == 0 || headerSize > 128*1024 {
|
| 37 |
+
return nil, errors.New("invalid native model header size")
|
| 38 |
+
}
|
| 39 |
+
header := make([]byte, int(headerSize))
|
| 40 |
+
if _, err := io.ReadFull(r, header); err != nil {
|
| 41 |
+
return nil, err
|
| 42 |
+
}
|
| 43 |
+
var raw map[string]json.RawMessage
|
| 44 |
+
if err := json.Unmarshal(header, &raw); err != nil {
|
| 45 |
+
return nil, err
|
| 46 |
+
}
|
| 47 |
+
delete(raw, "__metadata__")
|
| 48 |
+
if len(raw) != 202 {
|
| 49 |
+
return nil, errors.New("unexpected native model tensor count")
|
| 50 |
+
}
|
| 51 |
+
type spec struct {
|
| 52 |
+
Name string
|
| 53 |
+
DType string `json:"dtype"`
|
| 54 |
+
Shape []int `json:"shape"`
|
| 55 |
+
Offsets [2]int64 `json:"data_offsets"`
|
| 56 |
+
}
|
| 57 |
+
entries := make([]spec, 0, len(raw))
|
| 58 |
+
for name, data := range raw {
|
| 59 |
+
entry := spec{Name: name}
|
| 60 |
+
if err := json.Unmarshal(data, &entry); err != nil {
|
| 61 |
+
return nil, err
|
| 62 |
+
}
|
| 63 |
+
size := int64(1)
|
| 64 |
+
if entry.DType != "F32" || len(entry.Shape) < 1 || len(entry.Shape) > 2 {
|
| 65 |
+
return nil, errors.New("unsupported native model tensor")
|
| 66 |
+
}
|
| 67 |
+
for _, dim := range entry.Shape {
|
| 68 |
+
if dim <= 0 || dim > 128005 {
|
| 69 |
+
return nil, errors.New("invalid native model dimension")
|
| 70 |
+
}
|
| 71 |
+
size *= int64(dim)
|
| 72 |
+
}
|
| 73 |
+
if entry.Offsets[0] < 0 || size*4 != entry.Offsets[1]-entry.Offsets[0] || entry.Offsets[1] > nativeWeightsBytes-int64(headerSize)-8 {
|
| 74 |
+
return nil, errors.New("invalid native model offsets")
|
| 75 |
+
}
|
| 76 |
+
entries = append(entries, entry)
|
| 77 |
+
}
|
| 78 |
+
slices.SortFunc(entries, func(a, b spec) int {
|
| 79 |
+
if a.Offsets[0] < b.Offsets[0] {
|
| 80 |
+
return -1
|
| 81 |
+
}
|
| 82 |
+
if a.Offsets[0] > b.Offsets[0] {
|
| 83 |
+
return 1
|
| 84 |
+
}
|
| 85 |
+
return 0
|
| 86 |
+
})
|
| 87 |
+
result := make(map[string]nativeTensor, len(entries))
|
| 88 |
+
buffer := make([]byte, 64*1024)
|
| 89 |
+
var offset int64
|
| 90 |
+
for _, entry := range entries {
|
| 91 |
+
if entry.Offsets[0] != offset {
|
| 92 |
+
return nil, errors.New("native model has overlapping or missing data")
|
| 93 |
+
}
|
| 94 |
+
values := make([]float32, (entry.Offsets[1]-offset)/4)
|
| 95 |
+
for start := 0; start < len(values); {
|
| 96 |
+
if err := ctx.Err(); err != nil {
|
| 97 |
+
return nil, err
|
| 98 |
+
}
|
| 99 |
+
n := min(len(buffer)/4, len(values)-start)
|
| 100 |
+
if _, err := io.ReadFull(r, buffer[:n*4]); err != nil {
|
| 101 |
+
return nil, err
|
| 102 |
+
}
|
| 103 |
+
for i := 0; i < n; i++ {
|
| 104 |
+
value := math.Float32frombits(binary.LittleEndian.Uint32(buffer[i*4:]))
|
| 105 |
+
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
| 106 |
+
return nil, errors.New("non-finite native weight")
|
| 107 |
+
}
|
| 108 |
+
values[start+i] = value
|
| 109 |
+
}
|
| 110 |
+
start += n
|
| 111 |
+
}
|
| 112 |
+
result[entry.Name] = nativeTensor{Shape: entry.Shape, Data: values}
|
| 113 |
+
offset = entry.Offsets[1]
|
| 114 |
+
}
|
| 115 |
+
if n, err := r.Read(buffer[:1]); n != 0 || err != io.EOF || offset+int64(headerSize)+8 != nativeWeightsBytes {
|
| 116 |
+
return nil, errors.New("invalid native model length")
|
| 117 |
+
}
|
| 118 |
+
if hex.EncodeToString(digest.Sum(nil)) != nativeWeightsSHA256 {
|
| 119 |
+
return nil, errors.New("native model checksum mismatch")
|
| 120 |
+
}
|
| 121 |
+
return result, nil
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
func nativeWeight(tensors map[string]nativeTensor, name string, shape ...int) ([]float32, error) {
|
| 125 |
+
tensor, ok := tensors[name]
|
| 126 |
+
if !ok || !slices.Equal(tensor.Shape, shape) {
|
| 127 |
+
return nil, fmt.Errorf("unexpected native tensor %s", name)
|
| 128 |
+
}
|
| 129 |
+
return tensor.Data, nil
|
| 130 |
+
}
|
v3-ungated/native/stepsafety/native_workspace.go
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
// Inference reuses bounded scratch storage across layers and calls. In
|
| 4 |
+
// particular the six attention heads must not allocate 512x512 matrices on
|
| 5 |
+
// each of twelve layers. A pool also keeps independent callers race-free.
|
| 6 |
+
type nativeWorkspace struct {
|
| 7 |
+
x, q, k, v, a, attention, intermediate []float32
|
| 8 |
+
positions []int
|
| 9 |
+
heads [nativeHeads]nativeHeadWorkspace
|
| 10 |
+
}
|
| 11 |
+
type nativeHeadWorkspace struct {
|
| 12 |
+
q, k, v, vt, packedK, packedV, result []float32
|
| 13 |
+
scores, c2p, p2c []float32
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
func nativeBuffer(buffer []float32, n int) []float32 {
|
| 17 |
+
if cap(buffer) < n {
|
| 18 |
+
return make([]float32, n)
|
| 19 |
+
}
|
| 20 |
+
return buffer[:n]
|
| 21 |
+
}
|
| 22 |
+
func newNativeWorkspace() *nativeWorkspace {
|
| 23 |
+
h := func() []float32 { return make([]float32, maxSequenceLength*nativeHidden) }
|
| 24 |
+
w := &nativeWorkspace{x: h(), q: h(), k: h(), v: h(), a: h(), attention: h(), intermediate: make([]float32, maxSequenceLength*nativeIntermediate), positions: make([]int, maxSequenceLength*maxSequenceLength)}
|
| 25 |
+
for i := range w.heads {
|
| 26 |
+
s := func() []float32 { return make([]float32, maxSequenceLength*nativeHeadSize) }
|
| 27 |
+
matrix := func() []float32 { return make([]float32, maxSequenceLength*maxSequenceLength) }
|
| 28 |
+
w.heads[i] = nativeHeadWorkspace{q: s(), k: s(), v: s(), vt: s(), packedK: s(), packedV: s(), result: s(), scores: matrix(), c2p: matrix(), p2c: matrix()}
|
| 29 |
+
}
|
| 30 |
+
return w
|
| 31 |
+
}
|
v3-ungated/native/stepsafety/normalization.go
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"io"
|
| 6 |
+
"regexp"
|
| 7 |
+
"strings"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
var schemaHeader = regexp.MustCompile(`(?m)^([A-Za-z_][A-Za-z0-9_.:/-]*):[ \t]+`)
|
| 11 |
+
|
| 12 |
+
// candidateFields matches the saved joint-CE input view. Only API binding names
|
| 13 |
+
// and JSON representation change; executable strings, argument keys, request and
|
| 14 |
+
// observation text remain intact. Never modify the hook's shared maps in place.
|
| 15 |
+
func candidateFields(input Input) ([4]string, error) {
|
| 16 |
+
var fields [4]string
|
| 17 |
+
schema, err := candidateSchema(input.AvailableToolSchemas)
|
| 18 |
+
if err != nil {
|
| 19 |
+
return fields, err
|
| 20 |
+
}
|
| 21 |
+
definitions := schemaDefinitions(schema)
|
| 22 |
+
spellings := map[string]map[string]bool{}
|
| 23 |
+
for _, d := range definitions {
|
| 24 |
+
name := d["name"].(string)
|
| 25 |
+
key := lowerASCII(name)
|
| 26 |
+
if spellings[key] == nil {
|
| 27 |
+
spellings[key] = map[string]bool{}
|
| 28 |
+
}
|
| 29 |
+
spellings[key][name] = true
|
| 30 |
+
}
|
| 31 |
+
name := func(s string) string {
|
| 32 |
+
if len(spellings[lowerASCII(s)]) > 1 {
|
| 33 |
+
return s
|
| 34 |
+
}
|
| 35 |
+
return lowerASCII(s)
|
| 36 |
+
}
|
| 37 |
+
for _, d := range definitions {
|
| 38 |
+
d["name"] = name(d["name"].(string))
|
| 39 |
+
}
|
| 40 |
+
var schemaText string
|
| 41 |
+
if schema != nil {
|
| 42 |
+
if text, ok := schema.(string); ok {
|
| 43 |
+
schemaText = text
|
| 44 |
+
} else {
|
| 45 |
+
schemaText, err = compactSortedJSON(schema)
|
| 46 |
+
if err != nil {
|
| 47 |
+
return fields, err
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
history := input.InteractionHistory
|
| 52 |
+
if strings.TrimSpace(history) == "" {
|
| 53 |
+
history = "[]"
|
| 54 |
+
}
|
| 55 |
+
h, err := decodeCandidateJSON(history)
|
| 56 |
+
if err != nil {
|
| 57 |
+
return fields, backendError(ErrorInvalidHistory, nil)
|
| 58 |
+
}
|
| 59 |
+
events, ok := h.([]any)
|
| 60 |
+
if !ok || len(events) > maxHistoryEntries {
|
| 61 |
+
return fields, backendError(ErrorInvalidHistory, nil)
|
| 62 |
+
}
|
| 63 |
+
for _, event := range events {
|
| 64 |
+
e, ok := event.(map[string]any)
|
| 65 |
+
if !ok {
|
| 66 |
+
return fields, backendError(ErrorInvalidHistory, nil)
|
| 67 |
+
}
|
| 68 |
+
t, ok := e["tool"].(string)
|
| 69 |
+
if !ok || t == "" {
|
| 70 |
+
return fields, backendError(ErrorInvalidHistory, nil)
|
| 71 |
+
}
|
| 72 |
+
e["tool"] = name(t)
|
| 73 |
+
}
|
| 74 |
+
history, err = compactSortedJSON(events)
|
| 75 |
+
if err != nil {
|
| 76 |
+
return fields, err
|
| 77 |
+
}
|
| 78 |
+
args := input.ToolArguments
|
| 79 |
+
// These exact interfaces use title/description as call-display metadata.
|
| 80 |
+
// A TaskCreate description, WebFetch prompt, or arbitrary API payload is data.
|
| 81 |
+
switch input.ToolName {
|
| 82 |
+
case "Bash", "exec_command", "functions.exec_command", "mcp__workspace__bash", "mcp__cua_repl__js":
|
| 83 |
+
if values, ok := args.(map[string]any); ok {
|
| 84 |
+
copy := make(map[string]any, len(values))
|
| 85 |
+
for k, v := range values {
|
| 86 |
+
if k != "title" && k != "description" {
|
| 87 |
+
copy[k] = v
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
args = copy
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
arguments, err := compactSortedJSON(args)
|
| 94 |
+
if err != nil {
|
| 95 |
+
return fields, err
|
| 96 |
+
}
|
| 97 |
+
return [4]string{input.UserRequest, history, strings.TrimSpace("[TOOL_NAME]\n" + name(input.ToolName) + "\n[ARGUMENTS]\n" + arguments), schemaText}, nil
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
func lowerASCII(s string) string {
|
| 101 |
+
return strings.Map(func(r rune) rune {
|
| 102 |
+
if r >= 'A' && r <= 'Z' {
|
| 103 |
+
return r + ('a' - 'A')
|
| 104 |
+
}
|
| 105 |
+
return r
|
| 106 |
+
}, s)
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
func decodeCandidateJSON(text string) (any, error) {
|
| 110 |
+
d := json.NewDecoder(strings.NewReader(text))
|
| 111 |
+
d.UseNumber()
|
| 112 |
+
var v any
|
| 113 |
+
if err := d.Decode(&v); err != nil {
|
| 114 |
+
return nil, err
|
| 115 |
+
}
|
| 116 |
+
var extra any
|
| 117 |
+
if err := d.Decode(&extra); err != io.EOF {
|
| 118 |
+
return nil, backendError(ErrorUnsupportedText, nil)
|
| 119 |
+
}
|
| 120 |
+
return v, nil
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
func candidateSchema(value any) (any, error) {
|
| 124 |
+
if value == nil {
|
| 125 |
+
return nil, nil
|
| 126 |
+
}
|
| 127 |
+
if text, ok := value.(string); ok {
|
| 128 |
+
if strings.TrimSpace(text) == "" {
|
| 129 |
+
return nil, nil
|
| 130 |
+
}
|
| 131 |
+
if parsed, err := decodeCandidateJSON(text); err == nil {
|
| 132 |
+
switch parsed.(type) {
|
| 133 |
+
case []any, map[string]any:
|
| 134 |
+
return parsed, nil
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
matches := schemaHeader.FindAllStringSubmatchIndex(text, -1)
|
| 138 |
+
if len(matches) > 0 && strings.TrimSpace(text[:matches[0][0]]) == "" {
|
| 139 |
+
var definitions []any
|
| 140 |
+
for i, m := range matches {
|
| 141 |
+
end := len(text)
|
| 142 |
+
if i+1 < len(matches) {
|
| 143 |
+
end = matches[i+1][0]
|
| 144 |
+
}
|
| 145 |
+
definitions = append(definitions, map[string]any{"name": text[m[2]:m[3]], "description": strings.TrimSpace(text[m[1]:end])})
|
| 146 |
+
}
|
| 147 |
+
return definitions, nil
|
| 148 |
+
}
|
| 149 |
+
// Research also accepted Python literal containers. Hooks supply JSON;
|
| 150 |
+
// abstain on such unsupported containers instead of guessing a conversion.
|
| 151 |
+
if strings.HasPrefix(strings.TrimSpace(text), "[") || strings.HasPrefix(strings.TrimSpace(text), "{") {
|
| 152 |
+
return nil, backendError(ErrorUnsupportedText, nil)
|
| 153 |
+
}
|
| 154 |
+
return text, nil
|
| 155 |
+
}
|
| 156 |
+
text, err := compactSortedJSON(value)
|
| 157 |
+
if err != nil {
|
| 158 |
+
return nil, err
|
| 159 |
+
}
|
| 160 |
+
return decodeCandidateJSON(text)
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
func schemaDefinitions(value any) []map[string]any {
|
| 164 |
+
switch v := value.(type) {
|
| 165 |
+
case []any:
|
| 166 |
+
var result []map[string]any
|
| 167 |
+
for _, item := range v {
|
| 168 |
+
result = append(result, schemaDefinitions(item)...)
|
| 169 |
+
}
|
| 170 |
+
return result
|
| 171 |
+
case map[string]any:
|
| 172 |
+
if f, ok := v["function"].(map[string]any); ok {
|
| 173 |
+
return schemaDefinitions(f)
|
| 174 |
+
}
|
| 175 |
+
if t, ok := v["tools"].([]any); ok {
|
| 176 |
+
return schemaDefinitions(t)
|
| 177 |
+
}
|
| 178 |
+
_, named := v["name"].(string)
|
| 179 |
+
_, parameters := v["parameters"]
|
| 180 |
+
_, inputSchema := v["input_schema"]
|
| 181 |
+
_, description := v["description"]
|
| 182 |
+
if named && (parameters || inputSchema || description || len(v) == 1) {
|
| 183 |
+
return []map[string]any{v}
|
| 184 |
+
}
|
| 185 |
+
}
|
| 186 |
+
return nil
|
| 187 |
+
}
|
v3-ungated/native/stepsafety/packing.go
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"errors"
|
| 7 |
+
"strings"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
const maxSequenceLength = 512
|
| 11 |
+
|
| 12 |
+
var fieldBudgets = [4]int{96, 144, 128, 128}
|
| 13 |
+
var fieldMarkerIDs = [4]int64{128001, 128002, 128003, 128004}
|
| 14 |
+
|
| 15 |
+
type tokenEncoder interface {
|
| 16 |
+
encode(context.Context, string) ([]int64, error)
|
| 17 |
+
}
|
| 18 |
+
type packedInput struct {
|
| 19 |
+
IDs []int64
|
| 20 |
+
Mask []int64
|
| 21 |
+
HistoryOmitted bool
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func packInput(ctx context.Context, tokenizer tokenEncoder, input Input) (packedInput, error) {
|
| 25 |
+
var result packedInput
|
| 26 |
+
if ExcludedTool(input.ToolName) {
|
| 27 |
+
return result, backendError(ErrorExcludedTool, nil)
|
| 28 |
+
}
|
| 29 |
+
if err := validateInputBounds(input); err != nil {
|
| 30 |
+
return result, err
|
| 31 |
+
}
|
| 32 |
+
fields, err := candidateFields(input)
|
| 33 |
+
if err != nil {
|
| 34 |
+
return result, err
|
| 35 |
+
}
|
| 36 |
+
var selected [4][]int64
|
| 37 |
+
for _, index := range []int{2, 0, 3} {
|
| 38 |
+
tokens, err := tokenizer.encode(ctx, fields[index])
|
| 39 |
+
if err != nil {
|
| 40 |
+
return result, err
|
| 41 |
+
}
|
| 42 |
+
if len(tokens) > fieldBudgets[index] {
|
| 43 |
+
code := ErrorActionTooLarge
|
| 44 |
+
if index == 0 {
|
| 45 |
+
code = ErrorRequestTooLarge
|
| 46 |
+
}
|
| 47 |
+
if index == 3 {
|
| 48 |
+
code = ErrorSchemaTooLarge
|
| 49 |
+
}
|
| 50 |
+
return result, backendError(code, nil)
|
| 51 |
+
}
|
| 52 |
+
selected[index] = tokens
|
| 53 |
+
}
|
| 54 |
+
selected[1], result.HistoryOmitted, err = selectHistory(ctx, tokenizer, fields[1], fieldBudgets[1])
|
| 55 |
+
if err != nil {
|
| 56 |
+
return result, err
|
| 57 |
+
}
|
| 58 |
+
result.HistoryOmitted = result.HistoryOmitted || input.HistoryOmitted
|
| 59 |
+
result.IDs, result.Mask = assembleTokens(selected)
|
| 60 |
+
return result, nil
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Keep the training field order, special IDs, separators, and padding. Only
|
| 64 |
+
// history is truncated; admitted requests, actions, and schemas stay complete.
|
| 65 |
+
func assembleTokens(fields [4][]int64) ([]int64, []int64) {
|
| 66 |
+
ids := make([]int64, 0, maxSequenceLength)
|
| 67 |
+
ids = append(ids, 1)
|
| 68 |
+
for i, tokens := range fields {
|
| 69 |
+
ids = append(ids, fieldMarkerIDs[i])
|
| 70 |
+
ids = append(ids, tokens...)
|
| 71 |
+
ids = append(ids, 2)
|
| 72 |
+
}
|
| 73 |
+
mask := make([]int64, maxSequenceLength)
|
| 74 |
+
for i := range ids {
|
| 75 |
+
mask[i] = 1
|
| 76 |
+
}
|
| 77 |
+
ids = append(ids, make([]int64, maxSequenceLength-len(ids))...)
|
| 78 |
+
return ids, mask
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func selectHistory(ctx context.Context, tokenizer tokenEncoder, history string, budget int) ([]int64, bool, error) {
|
| 82 |
+
if strings.TrimSpace(history) == "" {
|
| 83 |
+
history = "[]"
|
| 84 |
+
}
|
| 85 |
+
var entries []json.RawMessage
|
| 86 |
+
if err := json.Unmarshal([]byte(history), &entries); err != nil {
|
| 87 |
+
return nil, false, backendError(ErrorInvalidHistory, nil)
|
| 88 |
+
}
|
| 89 |
+
if len(entries) > maxHistoryEntries {
|
| 90 |
+
return nil, false, backendError(ErrorInputTooLarge, nil)
|
| 91 |
+
}
|
| 92 |
+
selected := make([]json.RawMessage, 0, len(entries))
|
| 93 |
+
omitted := false
|
| 94 |
+
for _, entry := range entries {
|
| 95 |
+
var event struct {
|
| 96 |
+
Tool string `json:"tool"`
|
| 97 |
+
}
|
| 98 |
+
if err := json.Unmarshal(entry, &event); err != nil || event.Tool == "" {
|
| 99 |
+
return nil, omitted, backendError(ErrorInvalidHistory, nil)
|
| 100 |
+
}
|
| 101 |
+
if ExcludedTool(event.Tool) {
|
| 102 |
+
omitted = true
|
| 103 |
+
continue
|
| 104 |
+
}
|
| 105 |
+
selected = append(selected, entry)
|
| 106 |
+
}
|
| 107 |
+
encoded, err := compactSortedJSON(selected)
|
| 108 |
+
if err != nil {
|
| 109 |
+
return nil, omitted, err
|
| 110 |
+
}
|
| 111 |
+
tokens, err := tokenizer.encode(ctx, encoded)
|
| 112 |
+
if err != nil {
|
| 113 |
+
return nil, omitted, err
|
| 114 |
+
}
|
| 115 |
+
if len(tokens) > budget {
|
| 116 |
+
// Match the training history packer after removing excluded tools:
|
| 117 |
+
// first 72 + last 72 tokens at the fixed 144-token history budget.
|
| 118 |
+
// The model consumes token fragments; they need not form valid JSON.
|
| 119 |
+
head := (budget + 1) / 2
|
| 120 |
+
tail := budget - head
|
| 121 |
+
truncated := make([]int64, 0, budget)
|
| 122 |
+
tokens = append(append(truncated, tokens[:head]...), tokens[len(tokens)-tail:]...)
|
| 123 |
+
omitted = true
|
| 124 |
+
}
|
| 125 |
+
return tokens, omitted, nil
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// The schema field used Python's default JSON separators (unlike action and
|
| 129 |
+
// history). Insert spaces outside strings only to preserve that contract.
|
| 130 |
+
func schemaText(value any) (string, error) {
|
| 131 |
+
if value == nil {
|
| 132 |
+
return "", nil
|
| 133 |
+
}
|
| 134 |
+
if text, ok := value.(string); ok {
|
| 135 |
+
return text, nil
|
| 136 |
+
}
|
| 137 |
+
compact, err := compactSortedJSON(value)
|
| 138 |
+
if err != nil {
|
| 139 |
+
return "", err
|
| 140 |
+
}
|
| 141 |
+
var out strings.Builder
|
| 142 |
+
quoted, escaped := false, false
|
| 143 |
+
for _, r := range compact {
|
| 144 |
+
out.WriteRune(r)
|
| 145 |
+
if quoted {
|
| 146 |
+
if escaped {
|
| 147 |
+
escaped = false
|
| 148 |
+
} else if r == '\\' {
|
| 149 |
+
escaped = true
|
| 150 |
+
} else if r == '"' {
|
| 151 |
+
quoted = false
|
| 152 |
+
}
|
| 153 |
+
continue
|
| 154 |
+
}
|
| 155 |
+
if r == '"' {
|
| 156 |
+
quoted = true
|
| 157 |
+
} else if r == ':' || r == ',' {
|
| 158 |
+
out.WriteByte(' ')
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
return out.String(), nil
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// Match known file-tool names and namespace leaves, without excluding broad
|
| 165 |
+
// verbs such as read_email or write_message that the model can still score.
|
| 166 |
+
func ExcludedTool(name string) bool {
|
| 167 |
+
if len(name) > 256 {
|
| 168 |
+
return false
|
| 169 |
+
}
|
| 170 |
+
name = strings.ToLower(strings.TrimSpace(name))
|
| 171 |
+
if i := strings.LastIndex(name, "__"); i >= 0 {
|
| 172 |
+
name = name[i+2:]
|
| 173 |
+
}
|
| 174 |
+
if i := strings.LastIndexAny(name, ".:/"); i >= 0 {
|
| 175 |
+
name = name[i+1:]
|
| 176 |
+
}
|
| 177 |
+
switch name {
|
| 178 |
+
case "read", "write", "edit", "multiedit", "notebookedit", "apply_patch", "read_file", "read_text_file", "read_multiple_files", "write_file", "edit_file", "replace_file_content", "multi_replace_file_content":
|
| 179 |
+
return true
|
| 180 |
+
}
|
| 181 |
+
return false
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Reject large or malformed JSON values before allocating their serialization.
|
| 185 |
+
// Hook adapters supply JSON primitives; unsupported Go objects aren't inputs.
|
| 186 |
+
func boundedJSONSize(value any, remaining *int, depth int) error {
|
| 187 |
+
if depth > 64 || *remaining < 0 {
|
| 188 |
+
return backendError(ErrorInputTooLarge, nil)
|
| 189 |
+
}
|
| 190 |
+
*remaining -= 2
|
| 191 |
+
switch v := value.(type) {
|
| 192 |
+
case nil, bool, float64, float32, int, int64, json.Number:
|
| 193 |
+
case string:
|
| 194 |
+
*remaining -= len(v)
|
| 195 |
+
case map[string]any:
|
| 196 |
+
for key, item := range v {
|
| 197 |
+
*remaining -= len(key) + 3
|
| 198 |
+
if err := boundedJSONSize(item, remaining, depth+1); err != nil {
|
| 199 |
+
return err
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
case []any:
|
| 203 |
+
for _, item := range v {
|
| 204 |
+
if err := boundedJSONSize(item, remaining, depth+1); err != nil {
|
| 205 |
+
return err
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
default:
|
| 209 |
+
return backendError(ErrorInference, errors.New("unsupported JSON input"))
|
| 210 |
+
}
|
| 211 |
+
if *remaining < 0 {
|
| 212 |
+
return backendError(ErrorInputTooLarge, nil)
|
| 213 |
+
}
|
| 214 |
+
return nil
|
| 215 |
+
}
|
v3-ungated/native/stepsafety/request_gate.go
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"compress/gzip"
|
| 5 |
+
"context"
|
| 6 |
+
"crypto/sha256"
|
| 7 |
+
"encoding/hex"
|
| 8 |
+
"encoding/json"
|
| 9 |
+
"errors"
|
| 10 |
+
"golang.org/x/text/cases"
|
| 11 |
+
"golang.org/x/text/language"
|
| 12 |
+
"io"
|
| 13 |
+
"math"
|
| 14 |
+
"strings"
|
| 15 |
+
"unicode"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
const ErrorRequestNotInformational = "request_not_informational"
|
| 19 |
+
|
| 20 |
+
var requestGateModel struct {
|
| 21 |
+
Word, Char sparseVector
|
| 22 |
+
Intercept, Threshold float64
|
| 23 |
+
Calibrator scoreCalibration
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func loadRequestGateAssets() error {
|
| 27 |
+
f, e := nativeAssets.Open("model/native/request_gate.json.gz")
|
| 28 |
+
if e != nil {
|
| 29 |
+
return e
|
| 30 |
+
}
|
| 31 |
+
defer f.Close()
|
| 32 |
+
r, e := gzip.NewReader(f)
|
| 33 |
+
if e != nil {
|
| 34 |
+
return e
|
| 35 |
+
}
|
| 36 |
+
defer r.Close()
|
| 37 |
+
data, e := io.ReadAll(io.LimitReader(r, 5259026+1))
|
| 38 |
+
if e != nil {
|
| 39 |
+
return e
|
| 40 |
+
}
|
| 41 |
+
h := sha256.Sum256(data)
|
| 42 |
+
if len(data) != 5259026 || hex.EncodeToString(h[:]) != "d94b34f3c59803047365b594fe50531dd073d8ac14b5b8151c574b61d647f693" {
|
| 43 |
+
return errors.New("request gate checksum mismatch")
|
| 44 |
+
}
|
| 45 |
+
return json.Unmarshal(data, &requestGateModel)
|
| 46 |
+
}
|
| 47 |
+
func requestIntentProbability(text string) float64 {
|
| 48 |
+
c := requestGateModel.Calibrator
|
| 49 |
+
z := c.Coefficient*requestIntentMargin(text) + c.Intercept
|
| 50 |
+
if z >= 0 {
|
| 51 |
+
return 1 / (1 + math.Exp(-z))
|
| 52 |
+
}
|
| 53 |
+
v := math.Exp(z)
|
| 54 |
+
return v / (1 + v)
|
| 55 |
+
}
|
| 56 |
+
func requestIntentGate(ctx context.Context, text string) error {
|
| 57 |
+
if e := ctx.Err(); e != nil {
|
| 58 |
+
return e
|
| 59 |
+
}
|
| 60 |
+
p := requestIntentProbability(text)
|
| 61 |
+
if e := ctx.Err(); e != nil {
|
| 62 |
+
return e
|
| 63 |
+
}
|
| 64 |
+
if p < requestGateModel.Threshold {
|
| 65 |
+
return backendError(ErrorRequestNotInformational, nil)
|
| 66 |
+
}
|
| 67 |
+
return nil
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
func requestIntentMargin(text string) float64 {
|
| 71 |
+
// Match sklearn's lowercase Unicode word analyzer and char analyzer.
|
| 72 |
+
// The vocabulary is hashed to avoid shipping plaintext captured tokens.
|
| 73 |
+
text = cases.Lower(language.Und).String(text)
|
| 74 |
+
words := []string{}
|
| 75 |
+
run := []rune{}
|
| 76 |
+
flush := func() {
|
| 77 |
+
if len(run) >= 2 {
|
| 78 |
+
words = append(words, string(run))
|
| 79 |
+
}
|
| 80 |
+
run = run[:0]
|
| 81 |
+
}
|
| 82 |
+
for _, r := range text {
|
| 83 |
+
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' {
|
| 84 |
+
run = append(run, r)
|
| 85 |
+
} else {
|
| 86 |
+
flush()
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
flush()
|
| 90 |
+
counts := map[string]int{}
|
| 91 |
+
for i, word := range words {
|
| 92 |
+
counts[word]++
|
| 93 |
+
if i > 0 {
|
| 94 |
+
counts[words[i-1]+" "+word]++
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
score := requestGateModel.Intercept + vectorScore(counts, requestGateModel.Word)
|
| 98 |
+
var normalized strings.Builder
|
| 99 |
+
rr := []rune(text)
|
| 100 |
+
for i := 0; i < len(rr); {
|
| 101 |
+
j := i + 1
|
| 102 |
+
if pythonSpace(rr[i]) {
|
| 103 |
+
for j < len(rr) && pythonSpace(rr[j]) {
|
| 104 |
+
j++
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
if j-i >= 2 {
|
| 108 |
+
normalized.WriteByte(' ')
|
| 109 |
+
} else {
|
| 110 |
+
normalized.WriteRune(rr[i])
|
| 111 |
+
}
|
| 112 |
+
i = j
|
| 113 |
+
}
|
| 114 |
+
rr = []rune(normalized.String())
|
| 115 |
+
counts = map[string]int{}
|
| 116 |
+
for n := 3; n <= 5; n++ {
|
| 117 |
+
for i := 0; i+n <= len(rr); i++ {
|
| 118 |
+
counts[string(rr[i:i+n])]++
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
return score + vectorScore(counts, requestGateModel.Char)
|
| 122 |
+
}
|
v3-ungated/native/stepsafety/runtime_artifacts.go
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
// Official microsoft/onnxruntime release assets. Archive hashes are the
|
| 4 |
+
// publisher's GitHub asset digests; library hashes pin the extracted bytes.
|
| 5 |
+
// Intel macOS uses 1.23.2, the last upstream x86_64 release asset.
|
| 6 |
+
var runtimeArtifacts = map[string]runtimeSpec{
|
| 7 |
+
"darwin/arm64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-osx-arm64-1.24.4.tgz", Size: 30937282, SHA256: "93787795f47e1eee369182e43ed51b9e5da0878ab0346aecf4258979b8bba989"}, Library: artifactSpec{Name: "libonnxruntime.1.24.4.dylib", Size: 35418600, SHA256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"}},
|
| 8 |
+
"darwin/amd64": {Version: "1.23.2", Archive: artifactSpec{Name: "onnxruntime-osx-x86_64-1.23.2.tgz", Size: 11676322, SHA256: "d10359e16347b57d9959f7e80a225a5b4a66ed7d7e007274a15cae86836485a6"}, Library: artifactSpec{Name: "libonnxruntime.1.23.2.dylib", Size: 39742608, SHA256: "8c9c78de65ea3786f987c0d980e9c1b13a3a5fbc6b3e2965ba05b450e6e4c054"}},
|
| 9 |
+
"linux/arm64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-linux-aarch64-1.24.4.tgz", Size: 7181958, SHA256: "866109a9248d057671a039b9d725be4bd86888e3754140e6701ec621be9d4d7e"}, Library: artifactSpec{Name: "libonnxruntime.so.1.24.4", Size: 18625376, SHA256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"}},
|
| 10 |
+
"linux/amd64": {Version: "1.24.4", Archive: artifactSpec{Name: "onnxruntime-linux-x64-1.24.4.tgz", Size: 8155822, SHA256: "3a211fbea252c1e66290658f1b735b772056149f28321e71c308942cdb54b747"}, Library: artifactSpec{Name: "libonnxruntime.so.1.24.4", Size: 22159232, SHA256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"}},
|
| 11 |
+
}
|
v3-ungated/native/stepsafety/runtime_store.go
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"archive/tar"
|
| 5 |
+
"compress/gzip"
|
| 6 |
+
"context"
|
| 7 |
+
"encoding/json"
|
| 8 |
+
"errors"
|
| 9 |
+
"fmt"
|
| 10 |
+
"io"
|
| 11 |
+
"net/http"
|
| 12 |
+
"os"
|
| 13 |
+
"path/filepath"
|
| 14 |
+
"runtime"
|
| 15 |
+
"time"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
type runtimeSpec struct {
|
| 19 |
+
Version string
|
| 20 |
+
Archive artifactSpec
|
| 21 |
+
Library artifactSpec
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
func runtimeForPlatform() (runtimeSpec, error) {
|
| 25 |
+
spec, ok := runtimeArtifacts[runtime.GOOS+"/"+runtime.GOARCH]
|
| 26 |
+
if !ok {
|
| 27 |
+
return runtimeSpec{}, errors.New("step-safety ONNX Runtime is unsupported on this platform")
|
| 28 |
+
}
|
| 29 |
+
return spec, nil
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func runtimeDir(modelDir string) string {
|
| 33 |
+
return filepath.Join(modelDir, "runtime", runtime.GOOS+"-"+runtime.GOARCH)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
func ValidateRuntimeDir(modelDir string) (string, error) {
|
| 37 |
+
spec, err := runtimeForPlatform()
|
| 38 |
+
if err != nil {
|
| 39 |
+
return "", err
|
| 40 |
+
}
|
| 41 |
+
path := filepath.Join(runtimeDir(modelDir), spec.Library.Name)
|
| 42 |
+
if err := verifyArtifact(path, spec.Library); err != nil {
|
| 43 |
+
return "", err
|
| 44 |
+
}
|
| 45 |
+
return path, nil
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
func verifyArtifact(path string, spec artifactSpec) error {
|
| 49 |
+
info, err := os.Stat(path)
|
| 50 |
+
if err != nil {
|
| 51 |
+
return fmt.Errorf("artifact %s is unavailable: %w", spec.Name, err)
|
| 52 |
+
}
|
| 53 |
+
if !info.Mode().IsRegular() || info.Size() != spec.Size {
|
| 54 |
+
return fmt.Errorf("artifact %s has unexpected size", spec.Name)
|
| 55 |
+
}
|
| 56 |
+
hash, err := fileSHA256(path)
|
| 57 |
+
if err != nil || hash != spec.SHA256 {
|
| 58 |
+
return fmt.Errorf("artifact %s failed checksum verification", spec.Name)
|
| 59 |
+
}
|
| 60 |
+
return nil
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// InstallRuntime is called only by the explicit install command. Daemon
|
| 64 |
+
// startup is strictly offline and loads only this checksum-pinned library.
|
| 65 |
+
// archivePath supports air-gapped installation; an empty path downloads the
|
| 66 |
+
// platform-specific CPU archive from Microsoft's official GitHub release.
|
| 67 |
+
func InstallRuntime(ctx context.Context, modelDir, archivePath string) error {
|
| 68 |
+
spec, err := runtimeForPlatform()
|
| 69 |
+
if err != nil {
|
| 70 |
+
return err
|
| 71 |
+
}
|
| 72 |
+
destination := runtimeDir(modelDir)
|
| 73 |
+
if _, err := ValidateRuntimeDir(modelDir); err == nil {
|
| 74 |
+
return nil
|
| 75 |
+
}
|
| 76 |
+
if _, err := os.Stat(destination); err == nil {
|
| 77 |
+
return fmt.Errorf("ONNX Runtime destination exists but is invalid: %s", destination)
|
| 78 |
+
} else if !errors.Is(err, os.ErrNotExist) {
|
| 79 |
+
return err
|
| 80 |
+
}
|
| 81 |
+
parent := filepath.Dir(destination)
|
| 82 |
+
if err := os.MkdirAll(parent, 0o700); err != nil {
|
| 83 |
+
return err
|
| 84 |
+
}
|
| 85 |
+
temporary, err := os.MkdirTemp(parent, ".onnx-install-*")
|
| 86 |
+
if err != nil {
|
| 87 |
+
return err
|
| 88 |
+
}
|
| 89 |
+
defer os.RemoveAll(temporary)
|
| 90 |
+
if archivePath == "" {
|
| 91 |
+
archivePath = filepath.Join(temporary, spec.Archive.Name)
|
| 92 |
+
if err := downloadRuntime(ctx, spec, archivePath); err != nil {
|
| 93 |
+
return err
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
if err := verifyArtifact(archivePath, spec.Archive); err != nil {
|
| 97 |
+
return err
|
| 98 |
+
}
|
| 99 |
+
staged := filepath.Join(temporary, "runtime")
|
| 100 |
+
if err := os.Mkdir(staged, 0o700); err != nil {
|
| 101 |
+
return err
|
| 102 |
+
}
|
| 103 |
+
if err := extractRuntime(archivePath, staged, spec); err != nil {
|
| 104 |
+
return err
|
| 105 |
+
}
|
| 106 |
+
if err := verifyArtifact(filepath.Join(staged, spec.Library.Name), spec.Library); err != nil {
|
| 107 |
+
return err
|
| 108 |
+
}
|
| 109 |
+
provenance, err := json.MarshalIndent(spec, "", " ")
|
| 110 |
+
if err != nil {
|
| 111 |
+
return err
|
| 112 |
+
}
|
| 113 |
+
if err := os.WriteFile(filepath.Join(staged, "PROVENANCE.json"), append(provenance, '\n'), 0o600); err != nil {
|
| 114 |
+
return err
|
| 115 |
+
}
|
| 116 |
+
if err := ctx.Err(); err != nil {
|
| 117 |
+
return err
|
| 118 |
+
}
|
| 119 |
+
return os.Rename(staged, destination)
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
func downloadRuntime(ctx context.Context, spec runtimeSpec, path string) error {
|
| 123 |
+
url := "https://github.com/microsoft/onnxruntime/releases/download/v" + spec.Version + "/" + spec.Archive.Name
|
| 124 |
+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
| 125 |
+
if err != nil {
|
| 126 |
+
return err
|
| 127 |
+
}
|
| 128 |
+
client := &http.Client{Timeout: 2 * time.Minute}
|
| 129 |
+
response, err := client.Do(request)
|
| 130 |
+
if err != nil {
|
| 131 |
+
return err
|
| 132 |
+
}
|
| 133 |
+
defer response.Body.Close()
|
| 134 |
+
if response.StatusCode != http.StatusOK {
|
| 135 |
+
return fmt.Errorf("ONNX Runtime download returned HTTP %d", response.StatusCode)
|
| 136 |
+
}
|
| 137 |
+
out, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
| 138 |
+
if err != nil {
|
| 139 |
+
return err
|
| 140 |
+
}
|
| 141 |
+
written, copyErr := io.Copy(out, io.LimitReader(response.Body, spec.Archive.Size+1))
|
| 142 |
+
closeErr := out.Close()
|
| 143 |
+
if err := errors.Join(copyErr, closeErr); err != nil {
|
| 144 |
+
return err
|
| 145 |
+
}
|
| 146 |
+
if written != spec.Archive.Size {
|
| 147 |
+
return errors.New("ONNX Runtime download has unexpected size")
|
| 148 |
+
}
|
| 149 |
+
return nil
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
func extractRuntime(archivePath, destination string, spec runtimeSpec) error {
|
| 153 |
+
file, err := os.Open(archivePath)
|
| 154 |
+
if err != nil {
|
| 155 |
+
return err
|
| 156 |
+
}
|
| 157 |
+
defer file.Close()
|
| 158 |
+
zipped, err := gzip.NewReader(file)
|
| 159 |
+
if err != nil {
|
| 160 |
+
return err
|
| 161 |
+
}
|
| 162 |
+
defer zipped.Close()
|
| 163 |
+
archive := tar.NewReader(zipped)
|
| 164 |
+
root := spec.Archive.Name[:len(spec.Archive.Name)-len(".tgz")]
|
| 165 |
+
// Explicit allowlist: never extract archive paths or follow its symlinks.
|
| 166 |
+
wanted := map[string]string{
|
| 167 |
+
root + "/lib/" + spec.Library.Name: spec.Library.Name,
|
| 168 |
+
root + "/LICENSE": "LICENSE",
|
| 169 |
+
root + "/ThirdPartyNotices.txt": "ThirdPartyNotices.txt",
|
| 170 |
+
}
|
| 171 |
+
for {
|
| 172 |
+
header, err := archive.Next()
|
| 173 |
+
if errors.Is(err, io.EOF) {
|
| 174 |
+
break
|
| 175 |
+
}
|
| 176 |
+
if err != nil {
|
| 177 |
+
return err
|
| 178 |
+
}
|
| 179 |
+
name, ok := wanted[header.Name]
|
| 180 |
+
if !ok {
|
| 181 |
+
continue
|
| 182 |
+
}
|
| 183 |
+
limit := int64(4 * 1024 * 1024)
|
| 184 |
+
if name == spec.Library.Name {
|
| 185 |
+
limit = spec.Library.Size
|
| 186 |
+
}
|
| 187 |
+
if header.Typeflag != tar.TypeReg || header.Size < 1 || header.Size > limit {
|
| 188 |
+
return errors.New("unexpected ONNX Runtime archive entry")
|
| 189 |
+
}
|
| 190 |
+
out, err := os.OpenFile(filepath.Join(destination, name), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
| 191 |
+
if err != nil {
|
| 192 |
+
return err
|
| 193 |
+
}
|
| 194 |
+
_, copyErr := io.Copy(out, archive)
|
| 195 |
+
if err := errors.Join(copyErr, out.Close()); err != nil {
|
| 196 |
+
return err
|
| 197 |
+
}
|
| 198 |
+
delete(wanted, header.Name)
|
| 199 |
+
}
|
| 200 |
+
if len(wanted) != 0 {
|
| 201 |
+
return errors.New("ONNX Runtime archive is missing required library or license files")
|
| 202 |
+
}
|
| 203 |
+
return nil
|
| 204 |
+
}
|
v3-ungated/native/stepsafety/scoped.go
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"math"
|
| 7 |
+
"regexp"
|
| 8 |
+
"strings"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
const ErrorUnsupportedEffect = "unsupported_effect"
|
| 12 |
+
const ErrorMissingRequest = "missing_request"
|
| 13 |
+
const ErrorCompoundRequest = "compound_or_mutation_request"
|
| 14 |
+
|
| 15 |
+
var scopeCamel = regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
| 16 |
+
var scopeWords = regexp.MustCompile(`[a-z0-9]+`)
|
| 17 |
+
var requestWords = regexp.MustCompile(`[a-z]+`)
|
| 18 |
+
|
| 19 |
+
func scopeHas(words []string, options string) bool {
|
| 20 |
+
for _, option := range strings.Fields(options) {
|
| 21 |
+
for _, w := range words {
|
| 22 |
+
if w == option {
|
| 23 |
+
return true
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
return false
|
| 28 |
+
}
|
| 29 |
+
func scopePopulated(value any) bool {
|
| 30 |
+
switch v := value.(type) {
|
| 31 |
+
case string:
|
| 32 |
+
return strings.TrimSpace(v) != ""
|
| 33 |
+
case float64:
|
| 34 |
+
return !math.IsNaN(v) && !math.IsInf(v, 0)
|
| 35 |
+
case float32:
|
| 36 |
+
return !math.IsNaN(float64(v)) && !math.IsInf(float64(v), 0)
|
| 37 |
+
case int, int64:
|
| 38 |
+
return true
|
| 39 |
+
case json.Number:
|
| 40 |
+
n, e := v.Float64()
|
| 41 |
+
return e == nil && !math.IsNaN(n) && !math.IsInf(n, 0)
|
| 42 |
+
case []any:
|
| 43 |
+
if len(v) == 0 {
|
| 44 |
+
return false
|
| 45 |
+
}
|
| 46 |
+
for _, x := range v {
|
| 47 |
+
if !scopePopulated(x) {
|
| 48 |
+
return false
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
return true
|
| 52 |
+
}
|
| 53 |
+
return false
|
| 54 |
+
}
|
| 55 |
+
func recognizedScopeEffect(tool string, raw any) string {
|
| 56 |
+
args, ok := raw.(map[string]any)
|
| 57 |
+
if !ok {
|
| 58 |
+
return ""
|
| 59 |
+
}
|
| 60 |
+
ws := scopeWords.FindAllString(strings.ToLower(scopeCamel.ReplaceAllString(tool, "${1} ${2}")), -1)
|
| 61 |
+
if scopeHas(ws, "search find get list read inspect describe preview dry simulate check test") {
|
| 62 |
+
return ""
|
| 63 |
+
}
|
| 64 |
+
for _, key := range []string{"dry_run", "dryRun", "preview", "simulate"} {
|
| 65 |
+
if v, ok := args[key].(bool); ok && v {
|
| 66 |
+
return ""
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
keys := []string{}
|
| 70 |
+
for k, v := range args {
|
| 71 |
+
if scopePopulated(v) {
|
| 72 |
+
keys = append(keys, strings.ToLower(k))
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
if scopeHas(ws, "delete remove erase truncate drop wipe unlink") && scopeHas(ws, "file files email emails message messages record records database databases directory directories bucket buckets") && scopeHas(keys, "id ids file_id email_id message_id record_id path file filename name database bucket target records directory") {
|
| 76 |
+
return "destructive_object_change"
|
| 77 |
+
}
|
| 78 |
+
if scopeHas(ws, "send transfer pay") && scopeHas(ws, "money payment payments funds") && scopeHas(keys, "recipient to account destination recipient_id account_id") {
|
| 79 |
+
var amount float64
|
| 80 |
+
switch n := args["amount"].(type) {
|
| 81 |
+
case float64:
|
| 82 |
+
amount = n
|
| 83 |
+
case float32:
|
| 84 |
+
amount = float64(n)
|
| 85 |
+
case int:
|
| 86 |
+
amount = float64(n)
|
| 87 |
+
case int64:
|
| 88 |
+
amount = float64(n)
|
| 89 |
+
case json.Number:
|
| 90 |
+
amount, _ = n.Float64()
|
| 91 |
+
}
|
| 92 |
+
if amount > 0 && !math.IsNaN(amount) && !math.IsInf(amount, 0) {
|
| 93 |
+
return "money_transfer"
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
if scopeHas(ws, "change reset update set rotate") && scopeHas(ws, "password passwords credential credentials") {
|
| 97 |
+
for _, key := range []string{"password", "new_password", "newPassword", "credential"} {
|
| 98 |
+
if v, ok := args[key].(string); ok && strings.TrimSpace(v) != "" {
|
| 99 |
+
return "credential_change"
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
if scopeHas(ws, "send post publish forward") && scopeHas(ws, "email message mail") && scopeHas(keys, "recipient recipients to channel destination") && scopeHas(keys, "body content message text") {
|
| 104 |
+
return "outgoing_message"
|
| 105 |
+
}
|
| 106 |
+
if scopeHas(ws, "share grant invite") && scopeHas(ws, "file document folder access permission") && scopeHas(keys, "recipient recipients user users email") && scopeHas(keys, "file_id document_id folder_id path resource permission") {
|
| 107 |
+
return "access_share"
|
| 108 |
+
}
|
| 109 |
+
return ""
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
func scopedEligibility(input Input) error {
|
| 113 |
+
if recognizedScopeEffect(input.ToolName, input.ToolArguments) == "" {
|
| 114 |
+
return backendError(ErrorUnsupportedEffect, nil)
|
| 115 |
+
}
|
| 116 |
+
if strings.TrimSpace(input.UserRequest) == "" {
|
| 117 |
+
return backendError(ErrorMissingRequest, nil)
|
| 118 |
+
}
|
| 119 |
+
if scopeHas(requestWords.FindAllString(strings.ToLower(input.UserRequest), -1), "delete remove erase transfer send change update pay create modify write add book reserve schedule cancel install run execute deploy reset adjust approve archive buy checkout clear commit disburse discard dispose drop empty forward grant invite merge move order post publish purchase purge push refund reimburse rename reply reschedule respond restart rotate save set share start stop submit unlink upload wipe") {
|
| 120 |
+
return backendError(ErrorCompoundRequest, nil)
|
| 121 |
+
}
|
| 122 |
+
return nil
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// packScopedInput preserves the frozen bounded contextual representation.
|
| 126 |
+
// The current action is complete. Request uses its head, history and schema
|
| 127 |
+
// use head/tail. Eligibility is independent of tool effect and request intent.
|
| 128 |
+
// V3 ungated retains the full-action limit and action-agreement scoring.
|
| 129 |
+
func packScopedInput(ctx context.Context, tok tokenEncoder, input Input) (packedInput, error) {
|
| 130 |
+
var out packedInput
|
| 131 |
+
if err := validateInputBounds(input); err != nil {
|
| 132 |
+
return out, err
|
| 133 |
+
}
|
| 134 |
+
fields, err := candidateFields(input)
|
| 135 |
+
if err != nil {
|
| 136 |
+
return out, err
|
| 137 |
+
}
|
| 138 |
+
var selected [4][]int64
|
| 139 |
+
for i, text := range fields {
|
| 140 |
+
tokens, err := tok.encode(ctx, text)
|
| 141 |
+
if err != nil {
|
| 142 |
+
return out, err
|
| 143 |
+
}
|
| 144 |
+
budget := fieldBudgets[i]
|
| 145 |
+
if len(tokens) > budget {
|
| 146 |
+
if i == 2 {
|
| 147 |
+
return out, backendError(ErrorActionTooLarge, nil)
|
| 148 |
+
}
|
| 149 |
+
if i == 0 {
|
| 150 |
+
tokens = tokens[:budget]
|
| 151 |
+
} else {
|
| 152 |
+
head := (budget + 1) / 2
|
| 153 |
+
tokens = append(append([]int64{}, tokens[:head]...), tokens[len(tokens)-(budget-head):]...)
|
| 154 |
+
}
|
| 155 |
+
if i == 1 {
|
| 156 |
+
out.HistoryOmitted = true
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
selected[i] = tokens
|
| 160 |
+
}
|
| 161 |
+
out.IDs, out.Mask = assembleTokens(selected)
|
| 162 |
+
out.HistoryOmitted = out.HistoryOmitted || input.HistoryOmitted
|
| 163 |
+
return out, nil
|
| 164 |
+
}
|
v3-ungated/native/stepsafety/tokenizer.go
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"os"
|
| 6 |
+
"strings"
|
| 7 |
+
"unicode"
|
| 8 |
+
"unicode/utf8"
|
| 9 |
+
|
| 10 |
+
"golang.org/x/text/unicode/norm"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// This adapter is deliberately specific to the checksum-pinned DeBERTa
|
| 14 |
+
// tokenizer: the HF Replace -> NFC -> Strip normalizer and added special
|
| 15 |
+
// tokens here, Metaspace and Unigram segmentation in unigram.go.
|
| 16 |
+
type tokenizer struct{ unigram *unigram }
|
| 17 |
+
|
| 18 |
+
var specialTokens = []string{"[PAD]", "[CLS]", "[SEP]", "[UNK]", "[MASK]", "[USER_REQUEST]", "[INTERACTION_HISTORY]", "[CURRENT_ACTION]", "[TOOL_DESCRIPTIONS]"}
|
| 19 |
+
var specialTokenIDs = []int64{0, 1, 2, 3, 128000, 128001, 128002, 128003, 128004}
|
| 20 |
+
|
| 21 |
+
func loadTokenizer(path string) (*tokenizer, error) {
|
| 22 |
+
data, err := os.ReadFile(path)
|
| 23 |
+
if err != nil {
|
| 24 |
+
return nil, err
|
| 25 |
+
}
|
| 26 |
+
return parseTokenizer(data)
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func parseTokenizer(data []byte) (*tokenizer, error) {
|
| 30 |
+
unigram, err := parseUnigram(data)
|
| 31 |
+
if err != nil {
|
| 32 |
+
return nil, err
|
| 33 |
+
}
|
| 34 |
+
return &tokenizer{unigram: unigram}, nil
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func (t *tokenizer) encode(ctx context.Context, text string) ([]int64, error) {
|
| 38 |
+
var ids []int64
|
| 39 |
+
for len(text) > 0 {
|
| 40 |
+
if err := ctx.Err(); err != nil {
|
| 41 |
+
return nil, err
|
| 42 |
+
}
|
| 43 |
+
next, special := len(text), -1
|
| 44 |
+
for i, token := range specialTokens {
|
| 45 |
+
if at := strings.Index(text, token); at >= 0 && at < next {
|
| 46 |
+
next, special = at, i
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
// Added special tokens are extracted before normalizing each ordinary
|
| 50 |
+
// segment, matching normalized=false and lstrip/rstrip=false in HF.
|
| 51 |
+
segment := text[:next]
|
| 52 |
+
normalized := normalizeText(segment)
|
| 53 |
+
// Go NFC inserts CGJ for non-stream-safe combining sequences; HF NFC
|
| 54 |
+
// does not. Abstain on those rare inputs rather than silently changing
|
| 55 |
+
// the representation the checkpoint was trained on.
|
| 56 |
+
if strings.Count(normalized, "\u034f") != strings.Count(segment, "\u034f") {
|
| 57 |
+
return nil, backendError(ErrorUnsupportedText, nil)
|
| 58 |
+
}
|
| 59 |
+
ids = t.unigram.encode(normalized, ids)
|
| 60 |
+
if special < 0 {
|
| 61 |
+
break
|
| 62 |
+
}
|
| 63 |
+
ids = append(ids, specialTokenIDs[special])
|
| 64 |
+
text = text[next+len(specialTokens[special]):]
|
| 65 |
+
}
|
| 66 |
+
return ids, ctx.Err()
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
func normalizeText(text string) string {
|
| 70 |
+
var out strings.Builder
|
| 71 |
+
out.Grow(len(text))
|
| 72 |
+
for i := 0; i < len(text); {
|
| 73 |
+
r, size := utf8.DecodeRuneInString(text[i:])
|
| 74 |
+
if !unicode.IsSpace(r) {
|
| 75 |
+
out.WriteRune(r)
|
| 76 |
+
i += size
|
| 77 |
+
continue
|
| 78 |
+
}
|
| 79 |
+
start, count := i, 0
|
| 80 |
+
for i < len(text) {
|
| 81 |
+
r, size = utf8.DecodeRuneInString(text[i:])
|
| 82 |
+
if !unicode.IsSpace(r) {
|
| 83 |
+
break
|
| 84 |
+
}
|
| 85 |
+
count++
|
| 86 |
+
i += size
|
| 87 |
+
}
|
| 88 |
+
whitespace := text[start:i]
|
| 89 |
+
// Rust regex \s{2,}|[\n\r\t] uses Unicode whitespace; Go's regexp \s
|
| 90 |
+
// is ASCII-only, so using it here would silently change token IDs.
|
| 91 |
+
if count >= 2 || whitespace == "\n" || whitespace == "\r" || whitespace == "\t" {
|
| 92 |
+
out.WriteByte(' ')
|
| 93 |
+
} else {
|
| 94 |
+
out.WriteString(whitespace)
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
return strings.TrimRightFunc(nfcUnicode9(out.String()), unicode.IsSpace)
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// nfcUnicode9 matches HF NFC, whose tables are Unicode 9.0. HF treats every
|
| 101 |
+
// later character as an unassigned starter that never reorders or composes,
|
| 102 |
+
// while x/text's newer tables move or merge the ones in postUnicode9NFC. NFC
|
| 103 |
+
// cannot act across such a starter, so normalizing the text between them and
|
| 104 |
+
// copying them unchanged reproduces HF.
|
| 105 |
+
func nfcUnicode9(text string) string {
|
| 106 |
+
var out strings.Builder
|
| 107 |
+
for {
|
| 108 |
+
i := strings.IndexFunc(text, func(r rune) bool { return unicode.Is(postUnicode9NFC, r) })
|
| 109 |
+
if i < 0 {
|
| 110 |
+
break
|
| 111 |
+
}
|
| 112 |
+
_, size := utf8.DecodeRuneInString(text[i:])
|
| 113 |
+
out.WriteString(norm.NFC.String(text[:i]))
|
| 114 |
+
out.WriteString(text[i : i+size])
|
| 115 |
+
text = text[i+size:]
|
| 116 |
+
}
|
| 117 |
+
if out.Len() == 0 {
|
| 118 |
+
return norm.NFC.String(text)
|
| 119 |
+
}
|
| 120 |
+
out.WriteString(norm.NFC.String(text))
|
| 121 |
+
return out.String()
|
| 122 |
+
}
|
v3-ungated/native/stepsafety/types.go
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Package stepsafety runs the local, observe-only step-level tool safety pilot.
|
| 2 |
+
//
|
| 3 |
+
// The package deliberately has no enforcement API. A result is evidence for
|
| 4 |
+
// review, never an authorization decision, and every model failure degrades to
|
| 5 |
+
// an unavailable shadow result instead of an error returned to the hook path.
|
| 6 |
+
package stepsafety
|
| 7 |
+
|
| 8 |
+
import (
|
| 9 |
+
"context"
|
| 10 |
+
"encoding/json"
|
| 11 |
+
"errors"
|
| 12 |
+
"fmt"
|
| 13 |
+
"math"
|
| 14 |
+
"os"
|
| 15 |
+
"strconv"
|
| 16 |
+
"strings"
|
| 17 |
+
"time"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
const (
|
| 21 |
+
ModelVersion = "merlin-v3-ungated-20261104"
|
| 22 |
+
Threshold = 0.5
|
| 23 |
+
|
| 24 |
+
calibrationScale = 1.0
|
| 25 |
+
calibrationBias = 0.0
|
| 26 |
+
|
| 27 |
+
defaultTimeout = 250 * time.Millisecond
|
| 28 |
+
maxConfiguredTimeout = 500 * time.Millisecond
|
| 29 |
+
defaultStartupTimeout = 30 * time.Second
|
| 30 |
+
defaultConcurrency = 1
|
| 31 |
+
|
| 32 |
+
// Packing remains token-exact for admitted inputs, but hook-controlled
|
| 33 |
+
// strings must be bounded before the tokenizer sees them. Each limit is
|
| 34 |
+
// still orders of magnitude above its final token budget.
|
| 35 |
+
maxPretokenizedFieldBytes = 64 * 1024
|
| 36 |
+
maxPretokenizedTotalBytes = 128 * 1024
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
const (
|
| 40 |
+
DecisionSafe = "safe"
|
| 41 |
+
DecisionUnsafe = "unsafe"
|
| 42 |
+
DecisionUnavailable = "unavailable"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
const (
|
| 46 |
+
ErrorTimeout = "timeout"
|
| 47 |
+
ErrorUnavailable = "unavailable"
|
| 48 |
+
ErrorExcludedTool = "excluded_tool"
|
| 49 |
+
ErrorActionTooLarge = "action_too_large"
|
| 50 |
+
ErrorRequestTooLarge = "request_too_large"
|
| 51 |
+
ErrorSchemaTooLarge = "schema_too_large"
|
| 52 |
+
ErrorUnsupportedText = "unsupported_text"
|
| 53 |
+
ErrorInvalidHistory = "invalid_history"
|
| 54 |
+
ErrorInference = "inference_error"
|
| 55 |
+
ErrorConcurrency = "concurrency_timeout"
|
| 56 |
+
ErrorInvalidOutput = "invalid_output"
|
| 57 |
+
ErrorInputTooLarge = "input_too_large"
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
// Input is the complete production-available no-Thought representation. The
|
| 61 |
+
// action stays structured until Go renders the training form.
|
| 62 |
+
type Input struct {
|
| 63 |
+
UserRequest string `json:"user_request"`
|
| 64 |
+
RequestTooLarge bool `json:"request_too_large"`
|
| 65 |
+
InteractionHistory string `json:"interaction_history"`
|
| 66 |
+
HistoryOmitted bool `json:"history_omitted"`
|
| 67 |
+
ToolName string `json:"tool_name"`
|
| 68 |
+
ToolArguments any `json:"tool_arguments"`
|
| 69 |
+
AvailableToolSchemas any `json:"available_tool_schemas"`
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Evaluation is returned for every enabled PreToolUse call. UnsafeProbability
|
| 73 |
+
// retains its wire name for compatibility; V3 supplies an agreement score, not
|
| 74 |
+
// a calibrated risk probability. It is absent on abstention or failure; ShadowDecision then becomes unavailable and the real
|
| 75 |
+
// tool authorization remains untouched (fail open for this pilot).
|
| 76 |
+
type Evaluation struct {
|
| 77 |
+
UnsafeProbability *float64 `json:"unsafe_probability,omitempty"`
|
| 78 |
+
ShadowDecision string `json:"shadow_decision"`
|
| 79 |
+
Threshold float64 `json:"threshold"`
|
| 80 |
+
ModelVersion string `json:"model_version"`
|
| 81 |
+
LatencyMS float64 `json:"latency_ms"`
|
| 82 |
+
ErrorCode string `json:"error_code,omitempty"`
|
| 83 |
+
Enforced bool `json:"enforced"`
|
| 84 |
+
UserRequestPresent bool `json:"user_request_present"`
|
| 85 |
+
HistoryPresent bool `json:"history_present"`
|
| 86 |
+
HistoryOmitted bool `json:"history_omitted"`
|
| 87 |
+
ToolSchemasPresent bool `json:"tool_schemas_present"`
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
type Health struct {
|
| 91 |
+
Enabled bool `json:"enabled"`
|
| 92 |
+
Status string `json:"status"`
|
| 93 |
+
ModelVersion string `json:"model_version,omitempty"`
|
| 94 |
+
Device string `json:"device,omitempty"`
|
| 95 |
+
ErrorCode string `json:"error_code,omitempty"`
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
type Config struct {
|
| 99 |
+
Enabled bool
|
| 100 |
+
// ModelDir is retained for compatibility with historical development callers.
|
| 101 |
+
// Production inference always uses the embedded checkpoint.
|
| 102 |
+
ModelDir string
|
| 103 |
+
Timeout time.Duration
|
| 104 |
+
StartupTimeout time.Duration
|
| 105 |
+
MaxConcurrency int
|
| 106 |
+
ModelVersion string
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Backend is the narrow boundary between safety policy plumbing and local
|
| 110 |
+
// inference. Production uses the embedded Go model; tests can use deterministic fakes.
|
| 111 |
+
type Backend interface {
|
| 112 |
+
Infer(context.Context, Input) (InferenceResult, error)
|
| 113 |
+
Health(context.Context) (Health, error)
|
| 114 |
+
Close() error
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
type InferenceResult struct {
|
| 118 |
+
Logits [2]float64
|
| 119 |
+
HistoryOmitted bool
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
type codedError struct {
|
| 123 |
+
code string
|
| 124 |
+
err error
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
func (e codedError) Error() string { return e.err.Error() }
|
| 128 |
+
func (e codedError) Unwrap() error { return e.err }
|
| 129 |
+
|
| 130 |
+
func backendError(code string, err error) error {
|
| 131 |
+
if err == nil {
|
| 132 |
+
err = errors.New(code)
|
| 133 |
+
}
|
| 134 |
+
return codedError{code: code, err: err}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
type Evaluator struct {
|
| 138 |
+
backend Backend
|
| 139 |
+
timeout time.Duration
|
| 140 |
+
slots chan struct{}
|
| 141 |
+
modelVersion string
|
| 142 |
+
unavailable string
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
func ConfigFromEnv(_ string) (Config, error) {
|
| 146 |
+
// Advisory assessment is enabled unless the daemon explicitly opts out.
|
| 147 |
+
enabled, err := envBool("KONTEXT_STEP_SAFETY_SHADOW", true)
|
| 148 |
+
if err != nil {
|
| 149 |
+
return Config{}, err
|
| 150 |
+
}
|
| 151 |
+
if !enabled {
|
| 152 |
+
return Config{Enabled: false}, nil
|
| 153 |
+
}
|
| 154 |
+
timeout, err := envDuration("KONTEXT_STEP_SAFETY_TIMEOUT", defaultTimeout)
|
| 155 |
+
if err != nil {
|
| 156 |
+
return Config{}, err
|
| 157 |
+
}
|
| 158 |
+
if timeout > maxConfiguredTimeout {
|
| 159 |
+
return Config{}, fmt.Errorf("KONTEXT_STEP_SAFETY_TIMEOUT must not exceed %s", maxConfiguredTimeout)
|
| 160 |
+
}
|
| 161 |
+
startupTimeout, err := envDuration("KONTEXT_STEP_SAFETY_STARTUP_TIMEOUT", defaultStartupTimeout)
|
| 162 |
+
if err != nil {
|
| 163 |
+
return Config{}, err
|
| 164 |
+
}
|
| 165 |
+
concurrency, err := envInt("KONTEXT_STEP_SAFETY_MAX_CONCURRENCY", defaultConcurrency)
|
| 166 |
+
if err != nil {
|
| 167 |
+
return Config{}, err
|
| 168 |
+
}
|
| 169 |
+
if concurrency != 1 {
|
| 170 |
+
return Config{}, errors.New("KONTEXT_STEP_SAFETY_MAX_CONCURRENCY must be 1 for the singleton pilot session")
|
| 171 |
+
}
|
| 172 |
+
// One evaluation runs at a time. Admission stays within the same deadline.
|
| 173 |
+
return Config{
|
| 174 |
+
Enabled: enabled,
|
| 175 |
+
Timeout: timeout,
|
| 176 |
+
StartupTimeout: startupTimeout,
|
| 177 |
+
MaxConcurrency: concurrency,
|
| 178 |
+
ModelVersion: ModelVersion,
|
| 179 |
+
}, nil
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// New loads the embedded native Go checkpoint when enabled. Initialization
|
| 183 |
+
// failures produce an unavailable evaluator rather than failing daemon startup.
|
| 184 |
+
func New(ctx context.Context, cfg Config) *Evaluator {
|
| 185 |
+
if !cfg.Enabled {
|
| 186 |
+
return nil
|
| 187 |
+
}
|
| 188 |
+
cfg = normalizeConfig(cfg)
|
| 189 |
+
backend, err := newNativeBackend(ctx, cfg)
|
| 190 |
+
if err != nil {
|
| 191 |
+
return &Evaluator{
|
| 192 |
+
timeout: cfg.Timeout,
|
| 193 |
+
slots: make(chan struct{}, cfg.MaxConcurrency),
|
| 194 |
+
modelVersion: cfg.ModelVersion,
|
| 195 |
+
unavailable: errorCode(err),
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
return NewWithBackend(backend, cfg.Timeout, cfg.MaxConcurrency, cfg.ModelVersion)
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
func NewWithBackend(backend Backend, timeout time.Duration, maxConcurrency int, modelVersion string) *Evaluator {
|
| 202 |
+
if timeout <= 0 {
|
| 203 |
+
timeout = defaultTimeout
|
| 204 |
+
}
|
| 205 |
+
if maxConcurrency <= 0 {
|
| 206 |
+
maxConcurrency = defaultConcurrency
|
| 207 |
+
}
|
| 208 |
+
if strings.TrimSpace(modelVersion) == "" {
|
| 209 |
+
modelVersion = ModelVersion
|
| 210 |
+
}
|
| 211 |
+
return &Evaluator{
|
| 212 |
+
backend: backend,
|
| 213 |
+
timeout: timeout,
|
| 214 |
+
slots: make(chan struct{}, maxConcurrency),
|
| 215 |
+
modelVersion: modelVersion,
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
func normalizeConfig(cfg Config) Config {
|
| 220 |
+
if cfg.Timeout <= 0 {
|
| 221 |
+
cfg.Timeout = defaultTimeout
|
| 222 |
+
}
|
| 223 |
+
if cfg.StartupTimeout <= 0 {
|
| 224 |
+
cfg.StartupTimeout = defaultStartupTimeout
|
| 225 |
+
}
|
| 226 |
+
if cfg.MaxConcurrency <= 0 {
|
| 227 |
+
cfg.MaxConcurrency = defaultConcurrency
|
| 228 |
+
}
|
| 229 |
+
if strings.TrimSpace(cfg.ModelVersion) == "" {
|
| 230 |
+
cfg.ModelVersion = ModelVersion
|
| 231 |
+
}
|
| 232 |
+
return cfg
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
func (e *Evaluator) Evaluate(ctx context.Context, input Input) Evaluation {
|
| 236 |
+
started := time.Now()
|
| 237 |
+
result := Evaluation{
|
| 238 |
+
ShadowDecision: DecisionUnavailable,
|
| 239 |
+
Threshold: Threshold,
|
| 240 |
+
ModelVersion: ModelVersion,
|
| 241 |
+
Enforced: false,
|
| 242 |
+
UserRequestPresent: input.RequestTooLarge || strings.TrimSpace(input.UserRequest) != "",
|
| 243 |
+
HistoryPresent: historyIsPresent(input.InteractionHistory),
|
| 244 |
+
ToolSchemasPresent: input.AvailableToolSchemas != nil,
|
| 245 |
+
}
|
| 246 |
+
if e == nil {
|
| 247 |
+
return result
|
| 248 |
+
}
|
| 249 |
+
result.ModelVersion = e.modelVersion
|
| 250 |
+
result.HistoryOmitted = input.HistoryOmitted
|
| 251 |
+
if ExcludedTool(input.ToolName) {
|
| 252 |
+
result.ErrorCode = ErrorExcludedTool
|
| 253 |
+
result.LatencyMS = elapsedMilliseconds(started)
|
| 254 |
+
return result
|
| 255 |
+
}
|
| 256 |
+
if err := validateInputBounds(input); err != nil {
|
| 257 |
+
result.ErrorCode = errorCode(err)
|
| 258 |
+
result.LatencyMS = elapsedMilliseconds(started)
|
| 259 |
+
return result
|
| 260 |
+
}
|
| 261 |
+
if e.unavailable != "" || e.backend == nil {
|
| 262 |
+
result.ErrorCode = firstNonEmpty(e.unavailable, ErrorUnavailable)
|
| 263 |
+
result.LatencyMS = elapsedMilliseconds(started)
|
| 264 |
+
return result
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
callCtx, cancel := context.WithTimeout(ctx, e.timeout)
|
| 268 |
+
defer cancel()
|
| 269 |
+
select {
|
| 270 |
+
case e.slots <- struct{}{}:
|
| 271 |
+
case <-callCtx.Done():
|
| 272 |
+
result.ErrorCode = ErrorConcurrency
|
| 273 |
+
result.LatencyMS = elapsedMilliseconds(started)
|
| 274 |
+
return result
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
// Keep the slot until native execution has actually stopped, even when
|
| 278 |
+
// the hook deadline wins. This bounds outstanding work and prevents late
|
| 279 |
+
// results from being attributed to a later request. The native binding does
|
| 280 |
+
// not interrupt Run, so its completion must not extend the hook deadline.
|
| 281 |
+
type outcome struct {
|
| 282 |
+
value InferenceResult
|
| 283 |
+
err error
|
| 284 |
+
}
|
| 285 |
+
done := make(chan outcome, 1)
|
| 286 |
+
go func() {
|
| 287 |
+
defer func() { <-e.slots }()
|
| 288 |
+
value, err := e.backend.Infer(callCtx, input)
|
| 289 |
+
done <- outcome{value, err}
|
| 290 |
+
}()
|
| 291 |
+
var output outcome
|
| 292 |
+
select {
|
| 293 |
+
case output = <-done:
|
| 294 |
+
case <-callCtx.Done():
|
| 295 |
+
output.err = callCtx.Err()
|
| 296 |
+
}
|
| 297 |
+
result.LatencyMS = elapsedMilliseconds(started)
|
| 298 |
+
if callCtx.Err() != nil {
|
| 299 |
+
output.err = callCtx.Err()
|
| 300 |
+
}
|
| 301 |
+
if output.err != nil {
|
| 302 |
+
result.ErrorCode = errorCode(output.err)
|
| 303 |
+
return result
|
| 304 |
+
}
|
| 305 |
+
result.HistoryOmitted = result.HistoryOmitted || output.value.HistoryOmitted
|
| 306 |
+
logits := output.value.Logits
|
| 307 |
+
if math.IsNaN(logits[0]) || math.IsNaN(logits[1]) || math.IsInf(logits[0], 0) || math.IsInf(logits[1], 0) {
|
| 308 |
+
result.ErrorCode = ErrorInvalidOutput
|
| 309 |
+
return result
|
| 310 |
+
}
|
| 311 |
+
probability := CalibratedProbability(logits[1] - logits[0])
|
| 312 |
+
result.UnsafeProbability = &probability
|
| 313 |
+
result.ShadowDecision = DecisionSafe
|
| 314 |
+
if probability >= Threshold {
|
| 315 |
+
result.ShadowDecision = DecisionUnsafe
|
| 316 |
+
}
|
| 317 |
+
return result
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
func historyIsPresent(history string) bool {
|
| 321 |
+
normalized := strings.TrimSpace(history)
|
| 322 |
+
return normalized != "" && normalized != "[]"
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
func validateInputBounds(input Input) error {
|
| 326 |
+
if input.RequestTooLarge || len(input.UserRequest) > maxPretokenizedFieldBytes || len(input.InteractionHistory) > maxPretokenizedFieldBytes || len(input.ToolName) > 256 {
|
| 327 |
+
return backendError(ErrorInputTooLarge, nil)
|
| 328 |
+
}
|
| 329 |
+
for _, value := range []any{input.ToolArguments, input.AvailableToolSchemas} {
|
| 330 |
+
remaining := maxPretokenizedFieldBytes
|
| 331 |
+
if err := boundedJSONSize(value, &remaining, 0); err != nil {
|
| 332 |
+
return err
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
arguments, err := json.Marshal(input.ToolArguments)
|
| 337 |
+
if err != nil {
|
| 338 |
+
return backendError(ErrorInference, errors.New("step-safety arguments are not JSON encodable"))
|
| 339 |
+
}
|
| 340 |
+
schemas := []byte(nil)
|
| 341 |
+
if input.AvailableToolSchemas != nil {
|
| 342 |
+
schemas, err = json.Marshal(input.AvailableToolSchemas)
|
| 343 |
+
if err != nil {
|
| 344 |
+
return backendError(ErrorInference, errors.New("step-safety schemas are not JSON encodable"))
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
fields := []int{
|
| 348 |
+
len(input.UserRequest),
|
| 349 |
+
len(input.InteractionHistory),
|
| 350 |
+
len(input.ToolName) + len(arguments) + len("[TOOL_NAME]\n\n[ARGUMENTS]\n"),
|
| 351 |
+
len(schemas),
|
| 352 |
+
}
|
| 353 |
+
total := 0
|
| 354 |
+
for _, size := range fields {
|
| 355 |
+
if size > maxPretokenizedFieldBytes {
|
| 356 |
+
return backendError(ErrorInputTooLarge, errors.New("step-safety input field exceeds preprocessing bound"))
|
| 357 |
+
}
|
| 358 |
+
total += size
|
| 359 |
+
}
|
| 360 |
+
if total > maxPretokenizedTotalBytes {
|
| 361 |
+
return backendError(ErrorInputTooLarge, errors.New("step-safety input exceeds preprocessing bound"))
|
| 362 |
+
}
|
| 363 |
+
return nil
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
func CalibratedProbability(margin float64) float64 {
|
| 367 |
+
z := calibrationScale*margin + calibrationBias
|
| 368 |
+
if z >= 0 {
|
| 369 |
+
return 1 / (1 + math.Exp(-z))
|
| 370 |
+
}
|
| 371 |
+
exp := math.Exp(z)
|
| 372 |
+
return exp / (1 + exp)
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
func (e *Evaluator) Health(ctx context.Context) Health {
|
| 376 |
+
if e == nil {
|
| 377 |
+
return Health{Enabled: false, Status: "disabled"}
|
| 378 |
+
}
|
| 379 |
+
if e.unavailable != "" || e.backend == nil {
|
| 380 |
+
return Health{Enabled: true, Status: "unavailable", ModelVersion: e.modelVersion, ErrorCode: firstNonEmpty(e.unavailable, ErrorUnavailable)}
|
| 381 |
+
}
|
| 382 |
+
health, err := e.backend.Health(ctx)
|
| 383 |
+
if err != nil {
|
| 384 |
+
return Health{Enabled: true, Status: "unavailable", ModelVersion: e.modelVersion, ErrorCode: errorCode(err)}
|
| 385 |
+
}
|
| 386 |
+
health.Enabled = true
|
| 387 |
+
if health.ModelVersion == "" {
|
| 388 |
+
health.ModelVersion = e.modelVersion
|
| 389 |
+
}
|
| 390 |
+
return health
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
func (e *Evaluator) Close() error {
|
| 394 |
+
if e == nil || e.backend == nil {
|
| 395 |
+
return nil
|
| 396 |
+
}
|
| 397 |
+
return e.backend.Close()
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
func errorCode(err error) string {
|
| 401 |
+
if err == nil {
|
| 402 |
+
return ""
|
| 403 |
+
}
|
| 404 |
+
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
| 405 |
+
return ErrorTimeout
|
| 406 |
+
}
|
| 407 |
+
var coded codedError
|
| 408 |
+
if errors.As(err, &coded) && coded.code != "" {
|
| 409 |
+
return coded.code
|
| 410 |
+
}
|
| 411 |
+
return ErrorInference
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
func elapsedMilliseconds(started time.Time) float64 {
|
| 415 |
+
return float64(time.Since(started).Microseconds()) / 1000
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
func firstNonEmpty(values ...string) string {
|
| 419 |
+
for _, value := range values {
|
| 420 |
+
if value != "" {
|
| 421 |
+
return value
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
return ""
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
func envString(key, fallback string) string {
|
| 428 |
+
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
| 429 |
+
return value
|
| 430 |
+
}
|
| 431 |
+
return fallback
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
func envBool(key string, fallback bool) (bool, error) {
|
| 435 |
+
value := strings.TrimSpace(os.Getenv(key))
|
| 436 |
+
if value == "" {
|
| 437 |
+
return fallback, nil
|
| 438 |
+
}
|
| 439 |
+
parsed, err := strconv.ParseBool(value)
|
| 440 |
+
if err != nil {
|
| 441 |
+
return false, fmt.Errorf("%s must be a boolean: %w", key, err)
|
| 442 |
+
}
|
| 443 |
+
return parsed, nil
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
func envDuration(key string, fallback time.Duration) (time.Duration, error) {
|
| 447 |
+
value := strings.TrimSpace(os.Getenv(key))
|
| 448 |
+
if value == "" {
|
| 449 |
+
return fallback, nil
|
| 450 |
+
}
|
| 451 |
+
parsed, err := time.ParseDuration(value)
|
| 452 |
+
if err != nil || parsed <= 0 {
|
| 453 |
+
return 0, fmt.Errorf("%s must be a positive duration", key)
|
| 454 |
+
}
|
| 455 |
+
return parsed, nil
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
func envInt(key string, fallback int) (int, error) {
|
| 459 |
+
value := strings.TrimSpace(os.Getenv(key))
|
| 460 |
+
if value == "" {
|
| 461 |
+
return fallback, nil
|
| 462 |
+
}
|
| 463 |
+
parsed, err := strconv.Atoi(value)
|
| 464 |
+
if err != nil {
|
| 465 |
+
return 0, fmt.Errorf("%s must be an integer: %w", key, err)
|
| 466 |
+
}
|
| 467 |
+
return parsed, nil
|
| 468 |
+
}
|
v3-ungated/native/stepsafety/unicode9.go
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Code generated by UPDATE_GOLDEN=1 go test -run TestPostUnicode9TableCoversXText; DO NOT EDIT.
|
| 2 |
+
|
| 3 |
+
package stepsafety
|
| 4 |
+
|
| 5 |
+
import "unicode"
|
| 6 |
+
|
| 7 |
+
// postUnicode9NFC holds the runes Unicode 9.0 leaves unassigned that the
|
| 8 |
+
// Unicode 17.0.0 normalization tables reorder, compose or decompose.
|
| 9 |
+
var postUnicode9NFC = &unicode.RangeTable{
|
| 10 |
+
R16: []unicode.Range16{
|
| 11 |
+
{Lo: 0x07fd, Hi: 0x07fd, Stride: 1},
|
| 12 |
+
{Lo: 0x0897, Hi: 0x089f, Stride: 1},
|
| 13 |
+
{Lo: 0x08ca, Hi: 0x08d3, Stride: 1},
|
| 14 |
+
{Lo: 0x09fe, Hi: 0x09fe, Stride: 1},
|
| 15 |
+
{Lo: 0x0c3c, Hi: 0x0c3c, Stride: 1},
|
| 16 |
+
{Lo: 0x0d3b, Hi: 0x0d3c, Stride: 1},
|
| 17 |
+
{Lo: 0x0eba, Hi: 0x0eba, Stride: 1},
|
| 18 |
+
{Lo: 0x1715, Hi: 0x1715, Stride: 1},
|
| 19 |
+
{Lo: 0x1abf, Hi: 0x1add, Stride: 1},
|
| 20 |
+
{Lo: 0x1ae0, Hi: 0x1aeb, Stride: 1},
|
| 21 |
+
{Lo: 0x1df6, Hi: 0x1dfa, Stride: 1},
|
| 22 |
+
{Lo: 0xa82c, Hi: 0xa82c, Stride: 1},
|
| 23 |
+
},
|
| 24 |
+
R32: []unicode.Range32{
|
| 25 |
+
{Lo: 0x105c9, Hi: 0x105c9, Stride: 1},
|
| 26 |
+
{Lo: 0x105d2, Hi: 0x105d2, Stride: 1},
|
| 27 |
+
{Lo: 0x105da, Hi: 0x105da, Stride: 1},
|
| 28 |
+
{Lo: 0x105e4, Hi: 0x105e4, Stride: 1},
|
| 29 |
+
{Lo: 0x10d24, Hi: 0x10d27, Stride: 1},
|
| 30 |
+
{Lo: 0x10d69, Hi: 0x10d6d, Stride: 1},
|
| 31 |
+
{Lo: 0x10eab, Hi: 0x10eac, Stride: 1},
|
| 32 |
+
{Lo: 0x10efa, Hi: 0x10efb, Stride: 1},
|
| 33 |
+
{Lo: 0x10efd, Hi: 0x10eff, Stride: 1},
|
| 34 |
+
{Lo: 0x10f46, Hi: 0x10f50, Stride: 1},
|
| 35 |
+
{Lo: 0x10f82, Hi: 0x10f85, Stride: 1},
|
| 36 |
+
{Lo: 0x11070, Hi: 0x11070, Stride: 1},
|
| 37 |
+
{Lo: 0x1133b, Hi: 0x1133b, Stride: 1},
|
| 38 |
+
{Lo: 0x11382, Hi: 0x11385, Stride: 1},
|
| 39 |
+
{Lo: 0x1138b, Hi: 0x1138b, Stride: 1},
|
| 40 |
+
{Lo: 0x1138e, Hi: 0x1138e, Stride: 1},
|
| 41 |
+
{Lo: 0x11390, Hi: 0x11391, Stride: 1},
|
| 42 |
+
{Lo: 0x113b8, Hi: 0x113b8, Stride: 1},
|
| 43 |
+
{Lo: 0x113bb, Hi: 0x113bb, Stride: 1},
|
| 44 |
+
{Lo: 0x113c2, Hi: 0x113c2, Stride: 1},
|
| 45 |
+
{Lo: 0x113c5, Hi: 0x113c5, Stride: 1},
|
| 46 |
+
{Lo: 0x113c7, Hi: 0x113c9, Stride: 1},
|
| 47 |
+
{Lo: 0x113ce, Hi: 0x113d0, Stride: 1},
|
| 48 |
+
{Lo: 0x1145e, Hi: 0x1145e, Stride: 1},
|
| 49 |
+
{Lo: 0x11839, Hi: 0x1183a, Stride: 1},
|
| 50 |
+
{Lo: 0x11930, Hi: 0x11930, Stride: 1},
|
| 51 |
+
{Lo: 0x11935, Hi: 0x11935, Stride: 1},
|
| 52 |
+
{Lo: 0x11938, Hi: 0x11938, Stride: 1},
|
| 53 |
+
{Lo: 0x1193d, Hi: 0x1193e, Stride: 1},
|
| 54 |
+
{Lo: 0x11943, Hi: 0x11943, Stride: 1},
|
| 55 |
+
{Lo: 0x119e0, Hi: 0x119e0, Stride: 1},
|
| 56 |
+
{Lo: 0x11a34, Hi: 0x11a34, Stride: 1},
|
| 57 |
+
{Lo: 0x11a47, Hi: 0x11a47, Stride: 1},
|
| 58 |
+
{Lo: 0x11a99, Hi: 0x11a99, Stride: 1},
|
| 59 |
+
{Lo: 0x11d42, Hi: 0x11d42, Stride: 1},
|
| 60 |
+
{Lo: 0x11d44, Hi: 0x11d45, Stride: 1},
|
| 61 |
+
{Lo: 0x11d97, Hi: 0x11d97, Stride: 1},
|
| 62 |
+
{Lo: 0x11f41, Hi: 0x11f42, Stride: 1},
|
| 63 |
+
{Lo: 0x1611e, Hi: 0x16129, Stride: 1},
|
| 64 |
+
{Lo: 0x1612f, Hi: 0x1612f, Stride: 1},
|
| 65 |
+
{Lo: 0x16d63, Hi: 0x16d63, Stride: 1},
|
| 66 |
+
{Lo: 0x16d67, Hi: 0x16d6a, Stride: 1},
|
| 67 |
+
{Lo: 0x16ff0, Hi: 0x16ff1, Stride: 1},
|
| 68 |
+
{Lo: 0x1e08f, Hi: 0x1e08f, Stride: 1},
|
| 69 |
+
{Lo: 0x1e130, Hi: 0x1e136, Stride: 1},
|
| 70 |
+
{Lo: 0x1e2ae, Hi: 0x1e2ae, Stride: 1},
|
| 71 |
+
{Lo: 0x1e2ec, Hi: 0x1e2ef, Stride: 1},
|
| 72 |
+
{Lo: 0x1e4ec, Hi: 0x1e4ef, Stride: 1},
|
| 73 |
+
{Lo: 0x1e5ee, Hi: 0x1e5ef, Stride: 1},
|
| 74 |
+
{Lo: 0x1e6e3, Hi: 0x1e6e3, Stride: 1},
|
| 75 |
+
{Lo: 0x1e6e6, Hi: 0x1e6e6, Stride: 1},
|
| 76 |
+
{Lo: 0x1e6ee, Hi: 0x1e6ef, Stride: 1},
|
| 77 |
+
{Lo: 0x1e6f5, Hi: 0x1e6f5, Stride: 1},
|
| 78 |
+
},
|
| 79 |
+
}
|
v3-ungated/native/stepsafety/unigram.go
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package stepsafety
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"encoding/json"
|
| 5 |
+
"errors"
|
| 6 |
+
"math"
|
| 7 |
+
"strings"
|
| 8 |
+
"unicode/utf8"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
const metaspace = "▁"
|
| 12 |
+
|
| 13 |
+
// unigram is the Metaspace pre-tokenizer and Unigram model of the pinned
|
| 14 |
+
// tokenizer.json, matching HF tokenizers 0.22.2 (Unigram::encode_optimized)
|
| 15 |
+
// exactly: every vocab entry is matchable, including the literal "<0xNN>"
|
| 16 |
+
// and bracketed special pieces; path scores accumulate in float64; the first
|
| 17 |
+
// candidate to reach a position wins ties; an unknown rune scores the lowest
|
| 18 |
+
// piece score minus 10, and consecutive unknowns fuse into one [UNK].
|
| 19 |
+
//
|
| 20 |
+
// The vocabulary is a map whose keys share one backing string, about 8 MiB.
|
| 21 |
+
// A byte trie with a 256-pointer array per node held the same vocabulary in
|
| 22 |
+
// more than 600 MiB for the lifetime of the daemon.
|
| 23 |
+
type unigram struct {
|
| 24 |
+
ids map[string]int32
|
| 25 |
+
scores []float32 // the artifact's scores are exact float32 values
|
| 26 |
+
maxLen int // longest piece in bytes
|
| 27 |
+
unkID int32
|
| 28 |
+
unkScore float64
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
func parseUnigram(data []byte) (*unigram, error) {
|
| 32 |
+
var config struct {
|
| 33 |
+
Model struct {
|
| 34 |
+
Type string `json:"type"`
|
| 35 |
+
UnkID *int `json:"unk_id"`
|
| 36 |
+
ByteFallback bool `json:"byte_fallback"`
|
| 37 |
+
Vocab []json.RawMessage `json:"vocab"`
|
| 38 |
+
} `json:"model"`
|
| 39 |
+
}
|
| 40 |
+
if err := json.Unmarshal(data, &config); err != nil {
|
| 41 |
+
return nil, err
|
| 42 |
+
}
|
| 43 |
+
model := config.Model
|
| 44 |
+
if model.Type != "Unigram" || model.UnkID == nil || *model.UnkID < 0 || *model.UnkID >= len(model.Vocab) || model.ByteFallback {
|
| 45 |
+
return nil, errors.New("unsupported tokenizer model")
|
| 46 |
+
}
|
| 47 |
+
pieces := make([]string, len(model.Vocab))
|
| 48 |
+
scores := make([]float32, len(model.Vocab))
|
| 49 |
+
size, minScore := 0, math.Inf(1)
|
| 50 |
+
for i, raw := range model.Vocab {
|
| 51 |
+
var entry [2]json.RawMessage
|
| 52 |
+
var score float64
|
| 53 |
+
if err := json.Unmarshal(raw, &entry); err != nil {
|
| 54 |
+
return nil, err
|
| 55 |
+
}
|
| 56 |
+
if err := json.Unmarshal(entry[0], &pieces[i]); err != nil {
|
| 57 |
+
return nil, err
|
| 58 |
+
}
|
| 59 |
+
if err := json.Unmarshal(entry[1], &score); err != nil {
|
| 60 |
+
return nil, err
|
| 61 |
+
}
|
| 62 |
+
if float64(float32(score)) != score || pieces[i] == "" || !utf8.ValidString(pieces[i]) {
|
| 63 |
+
return nil, errors.New("unsupported tokenizer vocabulary entry")
|
| 64 |
+
}
|
| 65 |
+
scores[i] = float32(score)
|
| 66 |
+
minScore = math.Min(minScore, score)
|
| 67 |
+
size += len(pieces[i])
|
| 68 |
+
}
|
| 69 |
+
var backing strings.Builder
|
| 70 |
+
backing.Grow(size)
|
| 71 |
+
for _, piece := range pieces {
|
| 72 |
+
backing.WriteString(piece)
|
| 73 |
+
}
|
| 74 |
+
all := backing.String()
|
| 75 |
+
u := &unigram{ids: make(map[string]int32, len(pieces)), scores: scores, unkID: int32(*model.UnkID), unkScore: minScore - 10}
|
| 76 |
+
offset := 0
|
| 77 |
+
for i, piece := range pieces {
|
| 78 |
+
// HF resolves duplicate pieces to the later id.
|
| 79 |
+
u.ids[all[offset:offset+len(piece)]] = int32(i)
|
| 80 |
+
offset += len(piece)
|
| 81 |
+
u.maxLen = max(u.maxLen, len(piece))
|
| 82 |
+
}
|
| 83 |
+
return u, nil
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// encode pre-tokenizes one normalized segment that holds no special tokens:
|
| 87 |
+
// spaces become ▁, a ▁ is prepended unless present, and every word starts at
|
| 88 |
+
// a ▁. Each word is segmented independently.
|
| 89 |
+
func (u *unigram) encode(normalized string, ids []int64) []int64 {
|
| 90 |
+
if normalized == "" {
|
| 91 |
+
return ids
|
| 92 |
+
}
|
| 93 |
+
text := strings.ReplaceAll(normalized, " ", metaspace)
|
| 94 |
+
if !strings.HasPrefix(text, metaspace) {
|
| 95 |
+
text = metaspace + text
|
| 96 |
+
}
|
| 97 |
+
for len(text) > 0 {
|
| 98 |
+
word := text
|
| 99 |
+
if next := strings.Index(text[len(metaspace):], metaspace); next >= 0 {
|
| 100 |
+
word = text[:len(metaspace)+next]
|
| 101 |
+
}
|
| 102 |
+
ids = u.segment(word, ids)
|
| 103 |
+
text = text[len(word):]
|
| 104 |
+
}
|
| 105 |
+
return ids
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
type unigramNode struct {
|
| 109 |
+
score float64
|
| 110 |
+
start int // -1 until reached
|
| 111 |
+
id int32
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// segment appends the best-scoring segmentation of word.
|
| 115 |
+
func (u *unigram) segment(word string, ids []int64) []int64 {
|
| 116 |
+
n := len(word)
|
| 117 |
+
nodes := make([]unigramNode, n+1)
|
| 118 |
+
for i := range nodes {
|
| 119 |
+
nodes[i].start = -1
|
| 120 |
+
}
|
| 121 |
+
relax := func(end, start int, id int32, score float64) {
|
| 122 |
+
if node := &nodes[end]; node.start < 0 || score > node.score {
|
| 123 |
+
*node = unigramNode{score: score, start: start, id: id}
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
for start := 0; start < n; {
|
| 127 |
+
here := nodes[start].score
|
| 128 |
+
_, runeLen := utf8.DecodeRuneInString(word[start:])
|
| 129 |
+
single := false
|
| 130 |
+
for end := start + 1; end <= min(n, start+u.maxLen); end++ {
|
| 131 |
+
// Pieces are valid UTF-8, so they end on rune boundaries.
|
| 132 |
+
if end < n && !utf8.RuneStart(word[end]) {
|
| 133 |
+
continue
|
| 134 |
+
}
|
| 135 |
+
id, ok := u.ids[word[start:end]]
|
| 136 |
+
if !ok {
|
| 137 |
+
continue
|
| 138 |
+
}
|
| 139 |
+
relax(end, start, id, here+float64(u.scores[id]))
|
| 140 |
+
single = single || end-start == runeLen
|
| 141 |
+
}
|
| 142 |
+
if !single {
|
| 143 |
+
relax(start+runeLen, start, u.unkID, here+u.unkScore)
|
| 144 |
+
}
|
| 145 |
+
start += runeLen
|
| 146 |
+
}
|
| 147 |
+
first := len(ids)
|
| 148 |
+
for end := n; end > 0; {
|
| 149 |
+
node := nodes[end]
|
| 150 |
+
start, id := node.start, node.id
|
| 151 |
+
if id == u.unkID {
|
| 152 |
+
for start > 0 && nodes[start].id == u.unkID {
|
| 153 |
+
start = nodes[start].start
|
| 154 |
+
}
|
| 155 |
+
// HF maps the fused surface string back through the vocabulary.
|
| 156 |
+
if known, ok := u.ids[word[start:end]]; ok {
|
| 157 |
+
id = known
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
ids = append(ids, int64(id))
|
| 161 |
+
end = start
|
| 162 |
+
}
|
| 163 |
+
for i, j := first, len(ids)-1; i < j; i, j = i+1, j-1 {
|
| 164 |
+
ids[i], ids[j] = ids[j], ids[i]
|
| 165 |
+
}
|
| 166 |
+
return ids
|
| 167 |
+
}
|
v3-ungated/release_manifest.json
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"contains_private_examples": false,
|
| 3 |
+
"files": {
|
| 4 |
+
"LICENSE": {
|
| 5 |
+
"sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
|
| 6 |
+
"size": 11357
|
| 7 |
+
},
|
| 8 |
+
"NOTICE": {
|
| 9 |
+
"sha256": "5f307f4fc93430c9abe22bec00c00f3844291724bef614ac0a527a163ca5eab1",
|
| 10 |
+
"size": 271
|
| 11 |
+
},
|
| 12 |
+
"README.md": {
|
| 13 |
+
"sha256": "5d4285e2dd08af2d9a449889780ab2cf55c8ba13c1d8154aae04c734b2b04128",
|
| 14 |
+
"size": 4424
|
| 15 |
+
},
|
| 16 |
+
"evaluation.json": {
|
| 17 |
+
"sha256": "186ec26dd500b5f8791283a619200450c0f10e523ce3faca92a296f6acec1562",
|
| 18 |
+
"size": 1517
|
| 19 |
+
},
|
| 20 |
+
"native/cmd/merlin/main.go": {
|
| 21 |
+
"sha256": "0a9bbc83520ef46b1b3fc031f16fb2315f7a94d73e8dfef0d78374e64e466376",
|
| 22 |
+
"size": 1414
|
| 23 |
+
},
|
| 24 |
+
"native/go.mod": {
|
| 25 |
+
"sha256": "97ffbb6542f80e9cbc14bf46354388a7ba0ce18a1ad4822b64c750b7eda7abe4",
|
| 26 |
+
"size": 129
|
| 27 |
+
},
|
| 28 |
+
"native/go.sum": {
|
| 29 |
+
"sha256": "a121c8ef8f666600e1b0dde83144361a6a67d2850f36515c00d2860adfb53b23",
|
| 30 |
+
"size": 312
|
| 31 |
+
},
|
| 32 |
+
"native/stepsafety/agreement.go": {
|
| 33 |
+
"sha256": "a19ce6cba08771b455741ba7d2622334636c50b7ffde44fc98341c767f1d2363",
|
| 34 |
+
"size": 3798
|
| 35 |
+
},
|
| 36 |
+
"native/stepsafety/context.go": {
|
| 37 |
+
"sha256": "616f619ec4ff787d810085e1566836d13e5106ec6ee42a086411999329dd1563",
|
| 38 |
+
"size": 7264
|
| 39 |
+
},
|
| 40 |
+
"native/stepsafety/model/ATTRIBUTION.md": {
|
| 41 |
+
"sha256": "35596b6f09742aa91e8e11fea77170605db0e5d62f2c818f5c90a7b6d16b520e",
|
| 42 |
+
"size": 616
|
| 43 |
+
},
|
| 44 |
+
"native/stepsafety/model/LICENSE": {
|
| 45 |
+
"sha256": "646f8936b8ddcd14e13e578ff6857e368780b0d1a4f6066bee89211923a373e2",
|
| 46 |
+
"size": 1073
|
| 47 |
+
},
|
| 48 |
+
"native/stepsafety/model/PROVENANCE.json": {
|
| 49 |
+
"sha256": "dec7f4875979af4bab488e1aca9c6fcad97b8d56ea361a40dd1da8588ae1a3ab",
|
| 50 |
+
"size": 3748
|
| 51 |
+
},
|
| 52 |
+
"native/stepsafety/model/native/PROVENANCE.json": {
|
| 53 |
+
"sha256": "2432e56272cca72e32504a0c3605c2611f957ff58e70d23095bbbad7f3844739",
|
| 54 |
+
"size": 2622
|
| 55 |
+
},
|
| 56 |
+
"native/stepsafety/model/native/agreement.json.gz": {
|
| 57 |
+
"sha256": "6ffbd35e56a031df9f2ca08f7520486cc8ef65592c8d03cfe966fbd42523bbdf",
|
| 58 |
+
"size": 2290715
|
| 59 |
+
},
|
| 60 |
+
"native/stepsafety/model/native/candidate.json": {
|
| 61 |
+
"sha256": "776a05f6f1b3854527919a24a98815d17df8a7cd2928f0d05b4cfe286217c119",
|
| 62 |
+
"size": 3675
|
| 63 |
+
},
|
| 64 |
+
"native/stepsafety/model/native/manifest.json": {
|
| 65 |
+
"sha256": "ec4ca2647df5326ac2fc919e3eb71ad82fc5597e0e077db759b430ba941e5ee5",
|
| 66 |
+
"size": 1591
|
| 67 |
+
},
|
| 68 |
+
"native/stepsafety/model/native/request_gate.json.gz": {
|
| 69 |
+
"sha256": "acfb7025661b5a424d482aae1fd351f31f714bf9c22b7c18dcabb623ca2e72c3",
|
| 70 |
+
"size": 2492102
|
| 71 |
+
},
|
| 72 |
+
"native/stepsafety/model/native/tokenizer.json.gz": {
|
| 73 |
+
"sha256": "956061a0f96d88a118848b4f32e4e5fcdc8c363f4cbc223fdc175232668aa41c",
|
| 74 |
+
"size": 1736709
|
| 75 |
+
},
|
| 76 |
+
"native/stepsafety/model/native/weights.00.gz": {
|
| 77 |
+
"sha256": "2c69b3cccc79582da64dd47ab44835604b5b4f5c676b1102eacef17dc52cacad",
|
| 78 |
+
"size": 46014808
|
| 79 |
+
},
|
| 80 |
+
"native/stepsafety/model/native/weights.01.gz": {
|
| 81 |
+
"sha256": "129de5fde020ece20ce7cf2529564c779e486e29b0c4725f7ed09af086f6db04",
|
| 82 |
+
"size": 41825526
|
| 83 |
+
},
|
| 84 |
+
"native/stepsafety/model/native/weights.02.gz": {
|
| 85 |
+
"sha256": "7cb825c60a553c37d70ee6859d597a49b0afa0e34a2a243d62903da5d297f47e",
|
| 86 |
+
"size": 42596160
|
| 87 |
+
},
|
| 88 |
+
"native/stepsafety/model/native/weights.03.gz": {
|
| 89 |
+
"sha256": "d5a9ce4740491f638c497b8e88d6d4236a1f26b3debfa9f7566a22c960828b6c",
|
| 90 |
+
"size": 62319867
|
| 91 |
+
},
|
| 92 |
+
"native/stepsafety/model/native/weights.04.gz": {
|
| 93 |
+
"sha256": "9316ce90d589cba6e3ce02f5e643fa244ff7e7bb2b5117b8508f176f01ed3000",
|
| 94 |
+
"size": 13716366
|
| 95 |
+
},
|
| 96 |
+
"native/stepsafety/modelstore.go": {
|
| 97 |
+
"sha256": "d1dfee52f8984b8aafae9f6e519bd28e5cefc2dce274fee46cd3e4ba18b0fd92",
|
| 98 |
+
"size": 12453
|
| 99 |
+
},
|
| 100 |
+
"native/stepsafety/native_assets.go": {
|
| 101 |
+
"sha256": "73781cc21e22742983297f3e216ec4ede722e9c2281eaa8bfaee4515eb02a72b",
|
| 102 |
+
"size": 403
|
| 103 |
+
},
|
| 104 |
+
"native/stepsafety/native_assets_nomodel.go": {
|
| 105 |
+
"sha256": "d1c5d28591ae19d1f9003ffb29f56a7e2a0491c03a16ac2ab6c6400ec32e4dc6",
|
| 106 |
+
"size": 454
|
| 107 |
+
},
|
| 108 |
+
"native/stepsafety/native_backend.go": {
|
| 109 |
+
"sha256": "e94731e7b57cd0bdac1b045dd76f10fcf8690a5bec67ef2b5f1b97d7141f97b1",
|
| 110 |
+
"size": 3611
|
| 111 |
+
},
|
| 112 |
+
"native/stepsafety/native_linear_arm64.go": {
|
| 113 |
+
"sha256": "2a510eb9fc849bdf59d4e13fc9288dc14bca41034fb307f76f84ab30d49cb53b",
|
| 114 |
+
"size": 2108
|
| 115 |
+
},
|
| 116 |
+
"native/stepsafety/native_linear_arm64.s": {
|
| 117 |
+
"sha256": "f7d41e764cb85dc8b855d6a7f94834681c9aff236e67f3d1b04d2efa138de4b7",
|
| 118 |
+
"size": 1340
|
| 119 |
+
},
|
| 120 |
+
"native/stepsafety/native_linear_generic.go": {
|
| 121 |
+
"sha256": "c54f032dcb5a9b574cd539b838eacd83b71376b898edbb771e06ee5a191d947b",
|
| 122 |
+
"size": 690
|
| 123 |
+
},
|
| 124 |
+
"native/stepsafety/native_model.go": {
|
| 125 |
+
"sha256": "02fc641a5439fd359bd5f8dd5d6f26f4396ee2bbf0f3b06b6dac2c029989addc",
|
| 126 |
+
"size": 9618
|
| 127 |
+
},
|
| 128 |
+
"native/stepsafety/native_weights.go": {
|
| 129 |
+
"sha256": "104a1ea3851d97d920e1ae7b60d2b21be3ad58470a089fc5538eb2ba511af511",
|
| 130 |
+
"size": 4065
|
| 131 |
+
},
|
| 132 |
+
"native/stepsafety/native_workspace.go": {
|
| 133 |
+
"sha256": "6aa87fec23a4a88783990463dca83015512c93d9fcca1829e559cab729b64211",
|
| 134 |
+
"size": 1420
|
| 135 |
+
},
|
| 136 |
+
"native/stepsafety/normalization.go": {
|
| 137 |
+
"sha256": "b67a2422a587b6d59719dfb4027058b6f32495dec29ddb75d8524d1c0e1ba434",
|
| 138 |
+
"size": 5107
|
| 139 |
+
},
|
| 140 |
+
"native/stepsafety/packing.go": {
|
| 141 |
+
"sha256": "694c59c3d9e4724b912d34a7101d07fcf6fb5d2449883216eba11379e1c4f9d5",
|
| 142 |
+
"size": 5942
|
| 143 |
+
},
|
| 144 |
+
"native/stepsafety/request_gate.go": {
|
| 145 |
+
"sha256": "cb94dd7a32ab72e7a3f75017d297ee74a17ca0baa56283194f6f8febac2b7d83",
|
| 146 |
+
"size": 2758
|
| 147 |
+
},
|
| 148 |
+
"native/stepsafety/runtime_artifacts.go": {
|
| 149 |
+
"sha256": "cee5b858b63b60227d32ac241a47e7428df5f9d98a66c3397cf64f1fa035a3f4",
|
| 150 |
+
"size": 1672
|
| 151 |
+
},
|
| 152 |
+
"native/stepsafety/runtime_store.go": {
|
| 153 |
+
"sha256": "cb4fc9d15c372d303a76bd878ef2a992bb72eea728636792169c1f727ea5b25f",
|
| 154 |
+
"size": 5702
|
| 155 |
+
},
|
| 156 |
+
"native/stepsafety/scoped.go": {
|
| 157 |
+
"sha256": "00ffb738e773127099f9b25bcd4e7772e7b6796af17ef3ae76cb63960d1e2927",
|
| 158 |
+
"size": 5510
|
| 159 |
+
},
|
| 160 |
+
"native/stepsafety/tokenizer.go": {
|
| 161 |
+
"sha256": "3530fd17e4d5898636f19ff9246e6305b7e322373905d1bd83508042274f5b5c",
|
| 162 |
+
"size": 3663
|
| 163 |
+
},
|
| 164 |
+
"native/stepsafety/types.go": {
|
| 165 |
+
"sha256": "faa85a9c0438c6bffed0184d18d1531481d42500d0b7c9679f77d1a6a110c54a",
|
| 166 |
+
"size": 13934
|
| 167 |
+
},
|
| 168 |
+
"native/stepsafety/unicode9.go": {
|
| 169 |
+
"sha256": "3b110e09e6901a4d79cb085bbfaaeda4186e03c75a3ce196de29a3b7ee669c3d",
|
| 170 |
+
"size": 3024
|
| 171 |
+
},
|
| 172 |
+
"native/stepsafety/unigram.go": {
|
| 173 |
+
"sha256": "0a79d9c5c75306be59647997d8d29d2d2a917455f72a7fdf1d8a40bc62a09df0",
|
| 174 |
+
"size": 5041
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
"runtime": "merlin-v3-ungated-20261104",
|
| 178 |
+
"schema_version": 1,
|
| 179 |
+
"weights_candidate": "merlin-v3-precision-20261007"
|
| 180 |
+
}
|