Instructions to use apus-ailab/APUS-Reflex with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use apus-ailab/APUS-Reflex with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("apus-ailab/APUS-Reflex", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Add TypeSafe SDK serving with verified low/high vLLM routes
#1
by gump2049 - opened
- README.md +39 -0
- deployment/LICENSE +201 -0
- deployment/README.md +233 -0
- deployment/VALIDATION.md +48 -0
- deployment/contracts.py +301 -0
- deployment/download_model.py +47 -0
- deployment/export_low.py +354 -0
- deployment/gateway.py +430 -0
- deployment/requirements-test.txt +7 -0
- deployment/requirements.txt +10 -0
- deployment/run_backend.sh +48 -0
- deployment/sdk_example.py +59 -0
- deployment/smoke_sdk.py +209 -0
- deployment/tests/test_typesafe_gateway.py +364 -0
- deployment/tests/test_vllm_depth_export.py +237 -0
README.md
CHANGED
|
@@ -215,6 +215,45 @@ Keep `enable_thinking=False` for these short decision outputs. The original exam
|
|
| 215 |
|
| 216 |
This example returns standard chat-completion text through `choices[0].message.content`. It demonstrates **full-depth text serving**, not the specialized candidate-probability `/decide` gateway or `effort` routing. It does not enforce the three candidate labels; application-specific decision serving requires the corresponding prompt/compiler and output constraints. The server binds to localhost by default. Blackwell JIT compilation requires a compatible CUDA toolchain; the launcher disables the FlashInfer sampler as in the tested engine configuration.
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
## 6. Reproducing the Evaluation
|
| 219 |
|
| 220 |
Use the fixed dataset revision `7a63c3d6df2bdc6c8e82dfc566800dc318f0c36b`, preserve question and candidate order, and record the model revision, runtime, dtype, and compute budget. Keep reference labels out of the model input. Report decision accuracy separately from latency, and state precisely where timing starts and ends.
|
|
|
|
| 215 |
|
| 216 |
This example returns standard chat-completion text through `choices[0].message.content`. It demonstrates **full-depth text serving**, not the specialized candidate-probability `/decide` gateway or `effort` routing. It does not enforce the three candidate labels; application-specific decision serving requires the corresponding prompt/compiler and output constraints. The server binds to localhost by default. Blackwell JIT compilation requires a compatible CUDA toolchain; the launcher disables the FlashInfer sampler as in the tested engine configuration.
|
| 217 |
|
| 218 |
+
### TypeSafe SDK API with real low/high vLLM backends
|
| 219 |
+
|
| 220 |
+
The additional [TypeSafe deployment package](deployment/README.md) exposes `POST /v1/systemone` and `GET /v1/models` for the official `typesafe-sdk==0.7.0`. It routes `high` to the original merged **9B-5949 / 32-layer** model and `low` to a verified **16-layer** export of the same trained checkpoint. The original `serve_vllm.py` remains a standard, full-depth OpenAI text endpoint.
|
| 221 |
+
|
| 222 |
+
Follow [deployment/README.md](deployment/README.md) to download a pinned HF snapshot, export the trained low-depth model, and launch the two engines and gateway. The gateway checks actual model paths and layer configurations at startup; accepting an effort field alone is not treated as proof of shallow execution.
|
| 223 |
+
|
| 224 |
+
```python
|
| 225 |
+
import os
|
| 226 |
+
from typesafe_sdk import Choice, RetryPolicy, TypeSafeClient
|
| 227 |
+
|
| 228 |
+
with TypeSafeClient(
|
| 229 |
+
base_url="http://127.0.0.1:30013", # API root; do not append /v1
|
| 230 |
+
api_key=os.environ["JEV_API_KEY"], # Your local gateway key
|
| 231 |
+
model="APUS-OpenJev-v1-9B",
|
| 232 |
+
retry=RetryPolicy(max_retries=0),
|
| 233 |
+
timeout=60,
|
| 234 |
+
) as client:
|
| 235 |
+
result = client.system_one(
|
| 236 |
+
state="I was charged twice for my order. Please refund the duplicate charge.",
|
| 237 |
+
questions={"route": Choice(
|
| 238 |
+
instructions="Select the team that should handle this request.",
|
| 239 |
+
criteria={
|
| 240 |
+
"billing": "Payments, refunds, and billing issues",
|
| 241 |
+
"technical_support": "Technical problems and product errors",
|
| 242 |
+
"sales": "Pricing inquiries and purchase assistance",
|
| 243 |
+
},
|
| 244 |
+
)},
|
| 245 |
+
extra_body={"effort": "low"}, # Use high for 32 layers; default is high
|
| 246 |
+
)
|
| 247 |
+
print(result.choices["route"].choice)
|
| 248 |
+
print(result.raw_http_response.headers["x-openjev-executed-layers"])
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
For the **official hosted TypeSafe service**, use `base_url="https://api.typesafe.ai"`, `api_key=os.environ["TYPESAFE_API_KEY"]`, and an official model such as `model="jev-1.13.0"`. Omit `extra_body={"effort": ...}`: this depth control is an APUS extension. The SDK context manager keeps connections reusable across calls. Never reuse a hosted API credential as a local gateway secret.
|
| 252 |
+
|
| 253 |
+
Compatibility covers synchronous/asynchronous SDK calls, typed Choice/Noul/Score responses, model listing, and documented error handling within the model's [supported limits](deployment/README.md#contract-and-limits). This does not claim identical model behavior or official probability calibration. Multi-question calls are currently evaluated separately. Two-engine depth routing uses additional memory and does not share KV state or automatically continue a shallow request into deeper layers.
|
| 254 |
+
|
| 255 |
+
See [deployment validation](deployment/VALIDATION.md) for measured results and limitations. The deployment directory retains its source project's [Apache 2.0 license](deployment/LICENSE); this does not change the model weights' licenses.
|
| 256 |
+
|
| 257 |
## 6. Reproducing the Evaluation
|
| 258 |
|
| 259 |
Use the fixed dataset revision `7a63c3d6df2bdc6c8e82dfc566800dc318f0c36b`, preserve question and candidate order, and record the model revision, runtime, dtype, and compute budget. Keep reference labels out of the model input. Report decision accuracy separately from latency, and state precisely where timing starts and ends.
|
deployment/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.
|
deployment/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# APUS TypeSafe SDK gateway with explicit vLLM low/high routing
|
| 2 |
+
|
| 3 |
+
This directory adds the **official `typesafe-sdk==0.7.0` request/response protocol** to APUS-OpenJev. The local service is an APUS model, not the hosted Jev service. Its probabilities, calibration, quality, and billing do not inherit the hosted service's guarantees.
|
| 4 |
+
|
| 5 |
+
Two separately loaded vLLM engines serve the same trained checkpoint at different layer depths:
|
| 6 |
+
|
| 7 |
+
| Effort | Engine | Execution |
|
| 8 |
+
| --- | --- | --- |
|
| 9 |
+
| `high` (default) | Original merged `9B-5949` | 32 layers |
|
| 10 |
+
| `low` | Verified prefix export of that same checkpoint | 16 layers, original final norm and LM head |
|
| 11 |
+
|
| 12 |
+
This is **explicit request routing between two engines**. It is not automatic confidence-based early exit, shared weights between processes, hidden-state continuation, KV broadcast, or candidate-only LM-head execution. Both vLLM engines compute the full output head; the gateway extracts the requested candidate scores. A client-requested `low` never silently falls back to `high`.
|
| 13 |
+
|
| 14 |
+
## 1. Install and download the model
|
| 15 |
+
|
| 16 |
+
Use Linux, Python 3.12, CUDA-capable hardware, and vLLM 0.29.0. The 9B deployment work targets one RTX PRO 6000 Blackwell 96 GB; other hardware needs its own memory and numerical checks. Do not run the reference native runtime's Transformers 5.16.1 requirements in this vLLM environment.
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
python3.12 -m venv .venv
|
| 20 |
+
source .venv/bin/activate
|
| 21 |
+
pip install vllm==0.29.0 transformers==5.17.0 huggingface-hub==1.32.0 ninja==1.13.2
|
| 22 |
+
pip install fastapi==0.128.8 uvicorn==0.40.0 httpx==0.28.1 httpx2==2.13.0 typesafe-sdk==0.7.0
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
On Blackwell, configure the compatible CUDA 13 compiler in the same environment:
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
pip install nvidia-cuda-nvcc==13.4.92 nvidia-cuda-crt==13.4.92 nvidia-cuda-cccl==13.3.4.3.1
|
| 29 |
+
export CUDA_HOME="$(python -c 'import sysconfig; print(sysconfig.get_path("purelib") + "/nvidia/cu13")')"
|
| 30 |
+
export PATH="$CUDA_HOME/bin:$PATH"
|
| 31 |
+
"$CUDA_HOME/bin/nvcc" --version
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
The launch script disables the FlashInfer sampler to match the tested configuration. After downloading this directory, `pip install -r requirements.txt` installs the pinned deployment dependencies. `requirements-test.txt` supports the CPU gateway tests without installing a GPU runtime.
|
| 35 |
+
|
| 36 |
+
Resolve the repository revision once, then download by that immutable revision. The repository is a family bundle: use the **model subdirectory**, not its root.
|
| 37 |
+
|
| 38 |
+
```bash
|
| 39 |
+
export APUS_REVISION="$(python -c 'from huggingface_hub import HfApi; print(HfApi().model_info("apus-ailab/APUS-OpenJev-v1").sha)')"
|
| 40 |
+
hf download apus-ailab/APUS-OpenJev-v1 --revision "$APUS_REVISION" \
|
| 41 |
+
--include '9B-5949/*' 'deployment/*' --local-dir ./APUS-OpenJev-v1
|
| 42 |
+
cd ./APUS-OpenJev-v1/deployment
|
| 43 |
+
export APUS_FULL_MODEL="$(cd ../9B-5949 && pwd)"
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
If you already downloaded the deployment code separately, the included model-only helper performs the same immutable-revision resolution and prints the resulting model path:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
python download_model.py --local-dir ../models --revision "$APUS_REVISION"
|
| 50 |
+
# With this layout, set APUS_FULL_MODEL to ../models/9B-5949.
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Create a **new separate directory** for the trained 16-layer prefix. The exporter refuses an existing destination and verifies retained tensors; it never modifies the original weights.
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
export APUS_LOW_MODEL="$(dirname "$APUS_FULL_MODEL")/9B-5949-low16"
|
| 57 |
+
python export_low.py --source "$APUS_FULL_MODEL" --output "$APUS_LOW_MODEL"
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Start the full engine in terminal A, using an otherwise available GPU:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
export JEV_VLLM_PYTHON="$VIRTUAL_ENV/bin/python"
|
| 64 |
+
CUDA_VISIBLE_DEVICES=0 JEV_EFFORT=high \
|
| 65 |
+
JEV_MODEL_PATH="$APUS_FULL_MODEL" JEV_ENGINE_PORT=30002 \
|
| 66 |
+
JEV_GPU_MEMORY_UTILIZATION=0.70 JEV_MAX_NUM_SEQS=8 \
|
| 67 |
+
bash run_backend.sh
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
In terminal B, activate the same environment, set the same `CUDA_HOME` and `APUS_LOW_MODEL` paths, and start the shallow engine:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
export JEV_VLLM_PYTHON="$VIRTUAL_ENV/bin/python"
|
| 74 |
+
CUDA_VISIBLE_DEVICES=0 JEV_EFFORT=low \
|
| 75 |
+
JEV_MODEL_PATH="$APUS_LOW_MODEL" JEV_ENGINE_PORT=30004 \
|
| 76 |
+
JEV_GPU_MEMORY_UTILIZATION=0.22 JEV_MAX_NUM_SEQS=8 \
|
| 77 |
+
bash run_backend.sh
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
These defaults allocate separate engine budgets of 70% and 22% of the same 96 GB GPU. They are not appropriate if another workload already occupies that GPU. Compilation can take several minutes. The launcher verifies metadata and weight hashes before starting; this adds sequential disk reads. In terminal C, check both engines:
|
| 81 |
+
|
| 82 |
+
```bash
|
| 83 |
+
curl -f http://127.0.0.1:30002/health
|
| 84 |
+
curl -f http://127.0.0.1:30004/health
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Do not proceed until both are ready. Each foreground process can be stopped with Ctrl-C in its own terminal. Never reuse a port or GPU allocation belonging to another workload. The two engines must belong to the same source checkpoint; archive the export provenance and source revision together. The package uses `max_num_seqs=8` per engine; historical high-only measurements with 32 sequences are a different serving configuration and must not be copied as this setup's result.
|
| 88 |
+
|
| 89 |
+
## 2. Start the SDK gateway
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
export JEV_MODEL_PATH="$APUS_FULL_MODEL"
|
| 93 |
+
export JEV_HIGH_MODEL_PATH="$APUS_FULL_MODEL"
|
| 94 |
+
export JEV_LOW_MODEL_PATH="$APUS_LOW_MODEL"
|
| 95 |
+
export JEV_HIGH_URL=http://127.0.0.1:30002
|
| 96 |
+
export JEV_LOW_URL=http://127.0.0.1:30004
|
| 97 |
+
export JEV_HIGH_MODEL=apus-openjev-9b-high32
|
| 98 |
+
export JEV_LOW_MODEL=apus-openjev-9b-low16
|
| 99 |
+
export JEV_MODEL_NAME=APUS-OpenJev-v1-9B
|
| 100 |
+
# Supply your own local gateway secret; this is NOT your hosted TypeSafe API key.
|
| 101 |
+
export JEV_API_KEY='replace-with-your-local-gateway-secret'
|
| 102 |
+
python gateway.py --host 127.0.0.1 --port 30013
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
Startup requires both model directories on the same filesystem as the gateway. It checks 32/16-layer configs, merged indexes, completed low-export provenance, source/export metadata hashes, and each `/v1/models` alias/root against its configured artifact directory. Mismatches fail startup. The tokenizer must come from the full model directory. Engine URLs and aliases must be distinct. Depth headers report the selected verified backend configuration, not per-request layer-hook telemetry; live numerical evaluation remains necessary.
|
| 106 |
+
|
| 107 |
+
The gateway binds to localhost by default. Configure an authenticated HTTPS proxy before exposing it publicly. Backend URLs are operator configuration, never accepted from incoming requests. Both backend connections and the SDK client can reuse HTTP connections.
|
| 108 |
+
|
| 109 |
+
## 3. Use the official SDK against APUS
|
| 110 |
+
|
| 111 |
+
Use the **API root** as `base_url` — do not append `/v1`. The SDK appends `/v1/systemone` and `/v1/models` itself.
|
| 112 |
+
|
| 113 |
+
```python
|
| 114 |
+
import os
|
| 115 |
+
from typesafe_sdk import Choice, Noul, RetryPolicy, TypeSafeClient
|
| 116 |
+
|
| 117 |
+
with TypeSafeClient(
|
| 118 |
+
base_url="http://127.0.0.1:30013",
|
| 119 |
+
api_key=os.environ["JEV_API_KEY"],
|
| 120 |
+
model="APUS-OpenJev-v1-9B",
|
| 121 |
+
retry=RetryPolicy(max_retries=0),
|
| 122 |
+
) as client:
|
| 123 |
+
print(client.models.list())
|
| 124 |
+
result = client.system_one(
|
| 125 |
+
state={
|
| 126 |
+
"message": "My parcel arrived damaged yesterday. Can I get a refund?",
|
| 127 |
+
"policy": "Damaged items reported within 30 days qualify for a refund.",
|
| 128 |
+
},
|
| 129 |
+
questions={
|
| 130 |
+
"queue": Choice(
|
| 131 |
+
instructions="Choose the appropriate customer-support queue.",
|
| 132 |
+
criteria={"returns": "Returns and refunds", "billing": "Payments and invoices"},
|
| 133 |
+
),
|
| 134 |
+
"eligible": Noul(instructions="Is the customer eligible under this refund policy?"),
|
| 135 |
+
},
|
| 136 |
+
extra_body={"effort": "low"},
|
| 137 |
+
)
|
| 138 |
+
print(result.answers["queue"].choice)
|
| 139 |
+
print(result.answers["eligible"].noul)
|
| 140 |
+
print(result.raw_http_response.headers["x-openjev-executed-layers"])
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Use `extra_body={"effort": "high"}` for the full model, or omit it because high is the default. **`effort` is an APUS extension**, not a promise that the hosted TypeSafe API accepts the same field.
|
| 144 |
+
|
| 145 |
+
Run the complete English customer-support example (Choice + Noul + ordinal Score):
|
| 146 |
+
|
| 147 |
+
```bash
|
| 148 |
+
python sdk_example.py --effort high
|
| 149 |
+
python sdk_example.py --effort low
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
Async uses the same fields:
|
| 153 |
+
|
| 154 |
+
```python
|
| 155 |
+
import asyncio
|
| 156 |
+
import os
|
| 157 |
+
from typesafe_sdk import AsyncTypeSafeClient, Noul, RetryPolicy
|
| 158 |
+
|
| 159 |
+
async def main():
|
| 160 |
+
async with AsyncTypeSafeClient(
|
| 161 |
+
base_url="http://127.0.0.1:30013",
|
| 162 |
+
api_key=os.environ["JEV_API_KEY"],
|
| 163 |
+
model="APUS-OpenJev-v1-9B",
|
| 164 |
+
retry=RetryPolicy(max_retries=0),
|
| 165 |
+
) as client:
|
| 166 |
+
result = await client.system_one(
|
| 167 |
+
state="The customer received a damaged item yesterday.",
|
| 168 |
+
questions={"damaged": Noul(instructions="Did the customer report a damaged item?")},
|
| 169 |
+
extra_body={"effort": "high"},
|
| 170 |
+
)
|
| 171 |
+
print(result.answers["damaged"].noul)
|
| 172 |
+
|
| 173 |
+
asyncio.run(main())
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
## 4. Use the official hosted TypeSafe service separately
|
| 177 |
+
|
| 178 |
+
This snippet sends a real request to the hosted service and can incur charges. Keep its credentials separate from the local gateway secret. It deliberately does not send the APUS `effort` extension.
|
| 179 |
+
|
| 180 |
+
```python
|
| 181 |
+
import os
|
| 182 |
+
from typesafe_sdk import Choice, RetryPolicy, TypeSafeClient
|
| 183 |
+
|
| 184 |
+
with TypeSafeClient(
|
| 185 |
+
base_url="https://api.typesafe.ai",
|
| 186 |
+
api_key=os.environ["TYPESAFE_API_KEY"],
|
| 187 |
+
model="jev-latest",
|
| 188 |
+
retry=RetryPolicy(max_retries=0),
|
| 189 |
+
) as client:
|
| 190 |
+
result = client.system_one(
|
| 191 |
+
state="The customer was charged twice for the same order.",
|
| 192 |
+
questions={"queue": Choice(
|
| 193 |
+
instructions="Choose the appropriate customer-support queue.",
|
| 194 |
+
criteria={"billing": "Payments and invoices", "shipping": "Delivery and tracking"},
|
| 195 |
+
)},
|
| 196 |
+
)
|
| 197 |
+
print(result.answers["queue"].choice)
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
## Contract and limits
|
| 201 |
+
|
| 202 |
+
- Supported SDK 0.7.0 methods: synchronous/asynchronous `system_one` and `models.list`. This SDK version exposes no `models.retrieve` method.
|
| 203 |
+
- `GET /v1/models` lists the APUS model and `jev-latest` as an explicitly labeled **local compatibility alias**. Responses name the actual APUS model.
|
| 204 |
+
- `POST /v1/systemone` accepts `state`, `questions`, `model`, and the optional APUS `effort` extension only. Unknown fields, invalid effort, unsupported models, malformed or duplicate-key JSON fail explicitly.
|
| 205 |
+
- State: string / object / array. Instructions: string / object / array / null. Choice descriptions also allow null; null uses the candidate name. JSON is serialized deterministically; ordinary string descriptions remain unchanged.
|
| 206 |
+
- Choice: 2–16 candidates. Noul: yes/no; optional true/false descriptions affect the question's meaning. Canonical true/false descriptions retain the original binary prompt. Custom descriptions are included explicitly.
|
| 207 |
+
- Score: 1–10 ordered levels, selected by their descriptions; returns the expected zero-based index, probabilities, and the original legend. This differs from the trained binary `score_level` task and has **not established ordinal-score quality or calibration**. A single-level Score has only one possible result: score 0, probability 1, no GPU request, zero model tokens, and executed-layers header 0. Its confidence 1 denotes a degenerate distribution, not evidence that the rubric is objectively correct.
|
| 208 |
+
- At most 16 questions, 1 MiB request body, and 8,191 compiled prompt tokens per question (one output token reserved). Inputs are never silently truncated. Maximum 32 in-flight requests, with a 120-second request deadline.
|
| 209 |
+
- Each question is compiled and scored separately. Usage sums the processed prompt tokens and one scoring token per question; `billing_units` means local decision count, not official monetary billing.
|
| 210 |
+
- Confidence is normalized distribution concentration. Candidate probabilities are uncalibrated conditional softmax; neither quantity inherits official calibration semantics.
|
| 211 |
+
- Response shape uses `model`, `answers`, `usage`; Choice has `choice/confidence/probabilities`, Noul has `noul`, Score has `score/confidence/probabilities/legend`.
|
| 212 |
+
- Headers `x-openjev-effort`, `x-openjev-executed-layers`, and `x-openjev-engine-model` record the chosen route. `x-typesafe-request-id` is a local request identifier.
|
| 213 |
+
|
| 214 |
+
## Verification status
|
| 215 |
+
|
| 216 |
+
CPU integration tests use the **real official SDK 0.7.0**, both sync and async, through an ASGI gateway with a deterministic mocked inference backend. They verify protocol, routing, payload validation, and numerical response assembly; they are not GPU quality or performance measurements.
|
| 217 |
+
|
| 218 |
+
```bash
|
| 219 |
+
pip install -r requirements-test.txt
|
| 220 |
+
python -m pytest -q tests/test_typesafe_gateway.py
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
Before reporting GPU performance, record immutable model/export identities, prove 16/32-layer engine admission, run the same frozen requests through both routes, retain raw responses and collection timestamps, and compare against a reference using **the same compiled prompts**. In particular, a historic `score_level` record mapped to TypeSafe Noul changes its prompt primitive; do not silently compare it as byte-identical input.
|
| 224 |
+
|
| 225 |
+
Run a real service smoke test and preserve its raw requests, responses, UTC timestamps, and per-call latency:
|
| 226 |
+
|
| 227 |
+
```bash
|
| 228 |
+
python smoke_sdk.py --base-url http://127.0.0.1:30013 --output ./sdk-smoke-new-run
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
This sends five HTTP calls: models, high multi-question, low multi-question, an expected 422 for invalid effort, and an async low request. The two multi-question requests include all three primitives. Use a new output directory for every run. No authentication headers are saved.
|
| 232 |
+
|
| 233 |
+
Deployment code retains the included Apache 2.0 license and source notices. Model weights retain their applicable upstream licenses.
|
deployment/VALIDATION.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Deployment validation — 2026-09-21
|
| 2 |
+
|
| 3 |
+
The official TypeSafe Python SDK **0.7.0** successfully called this gateway on a real GPU deployment. `high` reached a 32-layer vLLM engine; `low` reached a separately exported 16-layer engine. This is explicit depth selection, not automatic confidence routing.
|
| 4 |
+
|
| 5 |
+
## Live SDK HTTP results
|
| 6 |
+
|
| 7 |
+
RTX PRO 6000 Blackwell 96 GB, vLLM 0.29.0, BF16, merged `9B-5949`, one GPU with two resident engines. Local loopback HTTP, one persistent SDK client, concurrency 1, retries disabled. Each effort ran the same frozen80 panel three times. Collection: **2026-09-21 13:46:20–13:46:52 UTC**. Timing covers SDK invocation through complete response validation; it is not streaming TTFT.
|
| 8 |
+
|
| 9 |
+
| SDK route | Successful | Accuracy per round | Mean | P50 | P95 | P99 |
|
| 10 |
+
| --- | ---: | --- | ---: | ---: | ---: | ---: |
|
| 11 |
+
| high / 32 layers | 240/240 | 67/80 in all three | 70.11 ms | 23.75 ms | 243.10 ms | 290.51 ms |
|
| 12 |
+
| low / 16 layers | 240/240 | 67/80 in all three | 49.17 ms | 22.01 ms | 154.61 ms | 177.58 ms |
|
| 13 |
+
|
| 14 |
+
Percentiles pool all 240 calls per route; they are not averages of per-round percentiles. Each run scored Browser 12/16, HelpSteer3 13/16, BoolQ 16/16, MNLI 12/16, and Score-derived Noul 14/16. There are only 80 distinct questions from 79 parent groups. These development questions have informed the project; neither significance nor general noninferiority follows from this result.
|
| 15 |
+
|
| 16 |
+
Mean latency fell **29.86%** (about **1.43×** speed) in this sequential local workload. Median improvement was much smaller. This does not establish a doubling of throughput or a public-network speedup. Serving budgets: high `gpu_memory_utilization=0.70`, `max_num_seqs=32`; low `0.22`, `max_num_seqs=8`; both max length 8192, prefix cache disabled. The supplied launcher defaults to 8 sequences for each route; its concurrent throughput must be benchmarked separately. Resident GPU memory was approximately 90,580 MiB with both engines loaded.
|
| 17 |
+
|
| 18 |
+
The SDK smoke separately passed model listing, multi-question Choice/Noul/Score at both efforts, invalid-effort error handling, and an asynchronous low request: **5/5 checks**. Full request/response bodies, UTC timestamps, route headers, and raw evaluation records were retained internally; no hosted API credentials are needed for this service.
|
| 19 |
+
|
| 20 |
+
## Numerical verification and limits
|
| 21 |
+
|
| 22 |
+
The exporter retained **548 tensors**, preserving their bytes, including the original embedding, final norm and LM head, while removing decoder layers 16–31. The source weights and metadata were checked against their release manifest. A native reference run used layer hooks to confirm that only layers 0–15 ran for all 80 questions.
|
| 23 |
+
|
| 24 |
+
A separate raw vLLM candidate-scoring comparison used the **same original compiled prompts**:
|
| 25 |
+
|
| 26 |
+
| Route | Correct | Same selected candidate as native reference | Maximum probability difference |
|
| 27 |
+
| --- | ---: | ---: | ---: |
|
| 28 |
+
| high32 | 67/80 | 80/80 | 0.06422 |
|
| 29 |
+
| low16 | 67/80 | 78/80 | 0.08855 |
|
| 30 |
+
|
| 31 |
+
Low changed two Browser decisions: one incorrect answer became correct and one correct answer became incorrect. Equal aggregate accuracy does **not** establish identical decisions or numerical parity. Original probability thresholds must be recalibrated for the deployed runtime. The first raw low request took about 1,552 ms before warm execution; SDK measurements above followed raw verification and smoke requests.
|
| 32 |
+
|
| 33 |
+
The wire benchmark uses the same state, instructions, candidate descriptions and frozen IDs as the official API requests, with the APUS model and effort extension. Historic binary `score_level` records map to TypeSafe **Noul**, changing the internal prompt primitive; the raw-reference comparison and SDK-wire comparison therefore answer different questions. Ordinal Score protocol smoke is not a scored quality benchmark for ordinal rubrics.
|
| 34 |
+
|
| 35 |
+
## Reproducibility
|
| 36 |
+
|
| 37 |
+
- Source HF family: `apus-ailab/APUS-OpenJev-v1`, model subdirectory `9B-5949/`; inspected source repository revision `c8a31769918d34e4142b9338b231a7bef59de3f3`. Serving-code publication does not change weights.
|
| 38 |
+
- Frozen panel SHA256: `d6dd45a9c8cb9f5b50254ac5f8f550e0ab1341e993c8657eb89232f64686ebf6`.
|
| 39 |
+
- Protocol tests: **12 passing**, real official SDK sync/async against a real ASGI gateway and mocked scoring engines.
|
| 40 |
+
- Export tests: **9 passing**, standalone CPU tiny-Qwen prefix/readout equivalence, retained tensor identity, source modification and weight-tampering rejection.
|
| 41 |
+
- Live collector tests: **2 passing**; independent offline verification checks all 480 saved requests, IDs, gold scoring, route headers and recorded timing.
|
| 42 |
+
- Serving bundle passes Ruff lint/format and shell syntax checks. A clean OS dependency installation was not tested; GPU tests used the existing verified Runpod environment.
|
| 43 |
+
|
| 44 |
+
The official hosted service was measured separately over a WAN: the earlier 240 pooled responses had P50/P95/P99 **280.80/376.56/830.45 ms**. A subsequent SDK single-connection 160-call test averaged **301.63 ms**. Those include geography/network effects and must not be divided by these loopback numbers to claim a model speedup.
|
| 45 |
+
|
| 46 |
+
## Compatibility boundaries
|
| 47 |
+
|
| 48 |
+
The gateway supports the SDK's synchronous/asynchronous `system_one` and `models.list` methods, typed responses, local bearer auth and documented errors. See README for candidate, context and request limits. This release does not assert unlimited compatibility with every request accepted by the hosted service, official calibration, automatic early exit, shared KV broadcast, or production Browser task completion.
|
deployment/contracts.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wire validation and deterministic semantic bridge to existing decision prompts."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
PROMPT_VERSION = "jev.dynamic.prompt.v2"
|
| 8 |
+
LABELS = tuple("ABCDEFGHIJKLMNOP")
|
| 9 |
+
BINARY_CRITERIA = [
|
| 10 |
+
{"id": "yes", "description": "The stated proposition is true."},
|
| 11 |
+
{"id": "no", "description": "The stated proposition is false."},
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def validate_request(record):
|
| 16 |
+
for key in ("id", "group_id", "state", "instructions"):
|
| 17 |
+
if not isinstance(record.get(key), str) or not record[key].strip():
|
| 18 |
+
raise ValueError(f"{key} must be a nonempty string")
|
| 19 |
+
if record.get("primitive") not in ("choice", "noul", "score_level"):
|
| 20 |
+
raise ValueError("unsupported primitive")
|
| 21 |
+
criteria = record.get("criteria")
|
| 22 |
+
if not isinstance(criteria, list) or not 2 <= len(criteria) <= len(LABELS):
|
| 23 |
+
raise ValueError("criteria must contain 2..16 candidates")
|
| 24 |
+
ids = []
|
| 25 |
+
for candidate in criteria:
|
| 26 |
+
if not isinstance(candidate, dict):
|
| 27 |
+
raise TypeError("candidate must be an object")
|
| 28 |
+
for key in ("id", "description"):
|
| 29 |
+
if not isinstance(candidate.get(key), str) or not candidate[key].strip():
|
| 30 |
+
raise ValueError(f"candidate {key} must be nonempty")
|
| 31 |
+
ids.append(candidate["id"])
|
| 32 |
+
if len(set(ids)) != len(ids):
|
| 33 |
+
raise ValueError("duplicate candidate ids")
|
| 34 |
+
if record["primitive"] != "choice" and criteria != BINARY_CRITERIA:
|
| 35 |
+
raise ValueError("noul and score_level require canonical yes/no criteria")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def validate_record(record):
|
| 39 |
+
validate_request(record)
|
| 40 |
+
if record.get("gold") not in [c["id"] for c in record["criteria"]]:
|
| 41 |
+
raise ValueError("gold must be a candidate id")
|
| 42 |
+
if not isinstance(record.get("provenance"), dict):
|
| 43 |
+
raise TypeError("provenance must be an object")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def label_mapping(record):
|
| 47 |
+
validate_request(record)
|
| 48 |
+
return dict(zip(LABELS, (c["id"] for c in record["criteria"])))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def render_prompt_parts(record):
|
| 52 |
+
"""Text prefix/suffix; callers MUST check tokenizer boundary equivalence."""
|
| 53 |
+
validate_request(record)
|
| 54 |
+
prefix = "Shared state:\n" + record["state"] + "\n\n"
|
| 55 |
+
task = {
|
| 56 |
+
"primitive": record["primitive"],
|
| 57 |
+
"instructions": record["instructions"],
|
| 58 |
+
"criteria": [
|
| 59 |
+
{"label": label, "description": candidate["description"]}
|
| 60 |
+
for label, candidate in zip(LABELS, record["criteria"])
|
| 61 |
+
],
|
| 62 |
+
}
|
| 63 |
+
suffix = json.dumps(task, ensure_ascii=False, sort_keys=True)
|
| 64 |
+
suffix += (
|
| 65 |
+
"\nReturn only the selected letter: "
|
| 66 |
+
+ ", ".join(LABELS[: len(record["criteria"])])
|
| 67 |
+
+ ".\nAnswer:"
|
| 68 |
+
)
|
| 69 |
+
return prefix, suffix
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def render_prompt(record):
|
| 73 |
+
return "".join(render_prompt_parts(record))
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
COMPILER_VERSION = "jev.typesafe.compiler.v1"
|
| 77 |
+
CONFIDENCE_POLICY = "normalized-entropy.v1"
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class ServingError(Exception):
|
| 81 |
+
def __init__(self, status, code, message):
|
| 82 |
+
super().__init__(message)
|
| 83 |
+
self.status, self.code, self.message = status, code, message
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def invalid(message):
|
| 87 |
+
raise ServingError(422, "invalid_request", message)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def text(value):
|
| 91 |
+
if isinstance(value, str) and value.strip():
|
| 92 |
+
return value
|
| 93 |
+
return json.dumps(value, ensure_ascii=False, sort_keys=True, allow_nan=False)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def content(value):
|
| 97 |
+
return isinstance(value, (str, dict, list))
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@dataclass(frozen=True)
|
| 101 |
+
class Question:
|
| 102 |
+
key: str
|
| 103 |
+
kind: str
|
| 104 |
+
record: dict
|
| 105 |
+
legend: dict | None = None
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def prepare_request(body, *, model_name, max_questions=16, allow_score=True):
|
| 109 |
+
if not isinstance(body, dict) or set(body) - {
|
| 110 |
+
"state",
|
| 111 |
+
"questions",
|
| 112 |
+
"model",
|
| 113 |
+
"effort",
|
| 114 |
+
}:
|
| 115 |
+
invalid("Expected state, questions, model and optional effort only")
|
| 116 |
+
if not isinstance(body.get("model", "jev-latest"), str) or body.get(
|
| 117 |
+
"model", "jev-latest"
|
| 118 |
+
) not in {model_name, "jev-latest"}:
|
| 119 |
+
raise ServingError(404, "unknown_model", "Model is not served")
|
| 120 |
+
if not content(body.get("state")):
|
| 121 |
+
invalid("state must be a string, object or array")
|
| 122 |
+
questions = body.get("questions")
|
| 123 |
+
if not isinstance(questions, dict) or not 1 <= len(questions) <= max_questions:
|
| 124 |
+
invalid("questions must be a nonempty map within the configured limit")
|
| 125 |
+
result = []
|
| 126 |
+
for key, question in questions.items():
|
| 127 |
+
if not isinstance(key, str) or not key.strip():
|
| 128 |
+
invalid("Question IDs must be nonempty strings")
|
| 129 |
+
if not isinstance(question, dict) or set(question) - {
|
| 130 |
+
"type",
|
| 131 |
+
"instructions",
|
| 132 |
+
"criteria",
|
| 133 |
+
}:
|
| 134 |
+
invalid("Invalid question fields")
|
| 135 |
+
kind = question.get("type")
|
| 136 |
+
instruction = question.get("instructions")
|
| 137 |
+
if instruction is not None and not content(instruction):
|
| 138 |
+
invalid("instructions must be text, object, array or null")
|
| 139 |
+
instruction = (
|
| 140 |
+
text(instruction)
|
| 141 |
+
if instruction is not None
|
| 142 |
+
else "Evaluate the state against the supplied criteria."
|
| 143 |
+
)
|
| 144 |
+
criteria = question.get("criteria")
|
| 145 |
+
legend = None
|
| 146 |
+
internal_kind = kind
|
| 147 |
+
if kind == "choice":
|
| 148 |
+
if not isinstance(criteria, dict) or not 2 <= len(criteria) <= len(LABELS):
|
| 149 |
+
invalid(
|
| 150 |
+
"This model supports 2..16 Choice candidates; none are truncated"
|
| 151 |
+
)
|
| 152 |
+
candidates = []
|
| 153 |
+
for name, description in criteria.items():
|
| 154 |
+
if not isinstance(name, str) or not name.strip():
|
| 155 |
+
invalid("Choice names must be nonempty strings")
|
| 156 |
+
if description is not None and not content(description):
|
| 157 |
+
invalid("Invalid Choice description")
|
| 158 |
+
candidates.append(
|
| 159 |
+
{
|
| 160 |
+
"id": name,
|
| 161 |
+
"description": name
|
| 162 |
+
if description is None
|
| 163 |
+
else text(description),
|
| 164 |
+
}
|
| 165 |
+
)
|
| 166 |
+
elif kind == "noul":
|
| 167 |
+
if criteria is not None:
|
| 168 |
+
if not isinstance(criteria, dict) or set(criteria) - {"true", "false"}:
|
| 169 |
+
invalid("Noul criteria accepts true and false descriptions")
|
| 170 |
+
if any(v is not None and not content(v) for v in criteria.values()):
|
| 171 |
+
invalid("Invalid Noul criterion")
|
| 172 |
+
if any(v is not None for v in criteria.values()) and criteria != {
|
| 173 |
+
"true": BINARY_CRITERIA[0]["description"],
|
| 174 |
+
"false": BINARY_CRITERIA[1]["description"],
|
| 175 |
+
}:
|
| 176 |
+
instruction = text(
|
| 177 |
+
{"question": instruction, "meaning_of_answers": criteria}
|
| 178 |
+
)
|
| 179 |
+
candidates = [dict(c) for c in BINARY_CRITERIA]
|
| 180 |
+
elif kind == "score":
|
| 181 |
+
if not allow_score:
|
| 182 |
+
raise ServingError(
|
| 183 |
+
422,
|
| 184 |
+
"unsupported_score",
|
| 185 |
+
"Ordinal Score is experimental and disabled; it is not score_level",
|
| 186 |
+
)
|
| 187 |
+
if not isinstance(criteria, list) or not 1 <= len(criteria) <= 10:
|
| 188 |
+
invalid("Score requires 1..10 ordered levels")
|
| 189 |
+
if any(not content(v) for v in criteria):
|
| 190 |
+
invalid("Invalid Score level")
|
| 191 |
+
legend = {str(i): v for i, v in enumerate(criteria)}
|
| 192 |
+
candidates = [{"id": k, "description": text(v)} for k, v in legend.items()]
|
| 193 |
+
internal_kind = "choice"
|
| 194 |
+
instruction = text(
|
| 195 |
+
{
|
| 196 |
+
"question": instruction,
|
| 197 |
+
"task": "Select the best matching ordered rubric level. Levels run from lowest to highest.",
|
| 198 |
+
}
|
| 199 |
+
)
|
| 200 |
+
else:
|
| 201 |
+
invalid("Unsupported question type")
|
| 202 |
+
record = {
|
| 203 |
+
"id": key,
|
| 204 |
+
"group_id": "serving",
|
| 205 |
+
"state": text(body["state"]),
|
| 206 |
+
"primitive": internal_kind,
|
| 207 |
+
"instructions": instruction,
|
| 208 |
+
"criteria": candidates,
|
| 209 |
+
}
|
| 210 |
+
result.append(Question(key, kind, record, legend))
|
| 211 |
+
return result
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
@dataclass(frozen=True)
|
| 215 |
+
class Compiled:
|
| 216 |
+
question: Question
|
| 217 |
+
prompt_ids: list[int]
|
| 218 |
+
candidate_token_ids: list[int]
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def compile_question(question, tokenizer, max_tokens=8192):
|
| 222 |
+
if question.kind == "score" and len(question.record["criteria"]) == 1:
|
| 223 |
+
return Compiled(question, [], [])
|
| 224 |
+
prompt = tokenizer.apply_chat_template(
|
| 225 |
+
[{"role": "user", "content": render_prompt(question.record)}],
|
| 226 |
+
tokenize=False,
|
| 227 |
+
add_generation_prompt=True,
|
| 228 |
+
enable_thinking=False,
|
| 229 |
+
)
|
| 230 |
+
ids = tokenizer.encode(prompt, add_special_tokens=False)
|
| 231 |
+
if not ids or len(ids) > max_tokens:
|
| 232 |
+
invalid("Compiled prompt exceeds token budget; no truncation is allowed")
|
| 233 |
+
special = set(tokenizer.all_special_ids)
|
| 234 |
+
if special.intersection(
|
| 235 |
+
tokenizer.encode(render_prompt(question.record), add_special_tokens=False)
|
| 236 |
+
):
|
| 237 |
+
invalid("Input contains reserved model control tokens")
|
| 238 |
+
tokens = []
|
| 239 |
+
for label in LABELS[: len(question.record["criteria"])]:
|
| 240 |
+
encoded = tokenizer.encode(label, add_special_tokens=False)
|
| 241 |
+
if (
|
| 242 |
+
len(encoded) != 1
|
| 243 |
+
or tokenizer.encode(prompt + label, add_special_tokens=False)
|
| 244 |
+
!= ids + encoded
|
| 245 |
+
):
|
| 246 |
+
invalid("Candidate is not one token at the answer boundary")
|
| 247 |
+
if encoded[0] in special or encoded[0] in tokens:
|
| 248 |
+
invalid("Invalid candidate token mapping")
|
| 249 |
+
tokens.append(encoded[0])
|
| 250 |
+
return Compiled(question, ids, tokens)
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def probabilities(scores):
|
| 254 |
+
if len(scores) < 2 or any(
|
| 255 |
+
type(v) not in (int, float) or not math.isfinite(v) for v in scores
|
| 256 |
+
):
|
| 257 |
+
raise ServingError(
|
| 258 |
+
502, "invalid_distribution", "Backend returned invalid candidate scores"
|
| 259 |
+
)
|
| 260 |
+
shift = max(scores)
|
| 261 |
+
values = [math.exp(v - shift) for v in scores]
|
| 262 |
+
total = sum(values)
|
| 263 |
+
return [v / total for v in values]
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def confidence(probs):
|
| 267 |
+
if len(probs) == 1:
|
| 268 |
+
return 1.0
|
| 269 |
+
entropy = -sum(p * math.log(p) for p in probs if p > 0)
|
| 270 |
+
return min(1.0, max(0.0, 1.0 - entropy / math.log(len(probs))))
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def answer(question, probs):
|
| 274 |
+
keys = [c["id"] for c in question.record["criteria"]]
|
| 275 |
+
if (
|
| 276 |
+
len(keys) != len(probs)
|
| 277 |
+
or any(
|
| 278 |
+
type(p) not in (int, float) or not math.isfinite(p) or not 0 <= p <= 1
|
| 279 |
+
for p in probs
|
| 280 |
+
)
|
| 281 |
+
or not math.isclose(sum(probs), 1, abs_tol=1e-6)
|
| 282 |
+
):
|
| 283 |
+
raise ServingError(
|
| 284 |
+
502,
|
| 285 |
+
"invalid_distribution",
|
| 286 |
+
"Backend distribution does not match candidates",
|
| 287 |
+
)
|
| 288 |
+
if question.kind == "noul":
|
| 289 |
+
return {"type": "noul", "noul": probs[keys.index("yes")]}
|
| 290 |
+
value = {
|
| 291 |
+
"type": question.kind,
|
| 292 |
+
"probabilities": dict(zip(keys, probs)),
|
| 293 |
+
"confidence": confidence(probs),
|
| 294 |
+
}
|
| 295 |
+
if question.kind == "choice":
|
| 296 |
+
value["choice"] = keys[max(range(len(probs)), key=probs.__getitem__)]
|
| 297 |
+
else:
|
| 298 |
+
value.update(
|
| 299 |
+
score=sum(i * p for i, p in enumerate(probs)), legend=question.legend
|
| 300 |
+
)
|
| 301 |
+
return value
|
deployment/download_model.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download only the merged 9B-5949 model from one immutable HF snapshot."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
p = argparse.ArgumentParser(description=__doc__)
|
| 11 |
+
p.add_argument("--repo", default="apus-ailab/APUS-OpenJev-v1")
|
| 12 |
+
p.add_argument("--revision", default="main")
|
| 13 |
+
p.add_argument("--local-dir", type=Path, required=True)
|
| 14 |
+
args = p.parse_args()
|
| 15 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 16 |
+
|
| 17 |
+
revision = HfApi().model_info(args.repo, revision=args.revision).sha
|
| 18 |
+
if not re.fullmatch("[a-f0-9]{40}", revision):
|
| 19 |
+
raise ValueError("Hub did not return an immutable commit")
|
| 20 |
+
root = Path(
|
| 21 |
+
snapshot_download(
|
| 22 |
+
repo_id=args.repo,
|
| 23 |
+
revision=revision,
|
| 24 |
+
allow_patterns=["9B-5949/*"],
|
| 25 |
+
local_dir=args.local_dir,
|
| 26 |
+
)
|
| 27 |
+
)
|
| 28 |
+
model = root / "9B-5949"
|
| 29 |
+
config = json.loads((model / "config.json").read_text())
|
| 30 |
+
if (
|
| 31 |
+
config["text_config"]["num_hidden_layers"] != 32
|
| 32 |
+
or (model / "adapter_config.json").exists()
|
| 33 |
+
):
|
| 34 |
+
raise ValueError("Expected the complete merged 32-layer model")
|
| 35 |
+
print(
|
| 36 |
+
json.dumps(
|
| 37 |
+
{
|
| 38 |
+
"repo": args.repo,
|
| 39 |
+
"revision": revision,
|
| 40 |
+
"model_path": str(model.resolve()),
|
| 41 |
+
}
|
| 42 |
+
)
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
main()
|
deployment/export_low.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export actual 16-layer Qwen3.5-9B prefix, preserving its shared norm/head."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import copy
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
import hashlib
|
| 7 |
+
import json
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import re
|
| 10 |
+
import shutil
|
| 11 |
+
|
| 12 |
+
LAYER = re.compile(r"^model\.language_model\.layers\.(\d+)\.")
|
| 13 |
+
READOUT = {
|
| 14 |
+
"model.language_model.embed_tokens.weight",
|
| 15 |
+
"model.language_model.norm.weight",
|
| 16 |
+
"lm_head.weight",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def file_sha(path):
|
| 21 |
+
h = hashlib.sha256()
|
| 22 |
+
with Path(path).open("rb") as f:
|
| 23 |
+
for block in iter(lambda: f.read(8 * 1024 * 1024), b""):
|
| 24 |
+
h.update(block)
|
| 25 |
+
return h.hexdigest()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def source_snapshot(source):
|
| 29 |
+
"""Stat all root model files; ignore unrelated nested evaluation directories."""
|
| 30 |
+
return {
|
| 31 |
+
p.name: {
|
| 32 |
+
"bytes": p.stat().st_size,
|
| 33 |
+
"mtime_ns": p.stat().st_mtime_ns,
|
| 34 |
+
"inode": p.stat().st_ino,
|
| 35 |
+
}
|
| 36 |
+
for p in sorted(Path(source).iterdir())
|
| 37 |
+
if p.is_file()
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def assert_source_unchanged(source, before):
|
| 42 |
+
if source_snapshot(source) != before:
|
| 43 |
+
raise ValueError(
|
| 44 |
+
"source model files changed during export; refusing verified publication"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def verify_artifact(path, depth):
|
| 49 |
+
"""Default launcher gate: rehash every weight, not only config and index."""
|
| 50 |
+
path = Path(path)
|
| 51 |
+
config = json.loads((path / "config.json").read_text())
|
| 52 |
+
text = config.get("text_config", {})
|
| 53 |
+
if (
|
| 54 |
+
config.get("model_type") != "qwen3_5"
|
| 55 |
+
or text.get("hidden_size") != 4096
|
| 56 |
+
or text.get("vocab_size") != 248320
|
| 57 |
+
or text.get("num_hidden_layers") != depth
|
| 58 |
+
or len(text.get("layer_types", [])) != depth
|
| 59 |
+
or (path / "adapter_config.json").exists()
|
| 60 |
+
):
|
| 61 |
+
raise ValueError("backend architecture/depth mismatch")
|
| 62 |
+
if depth == 32:
|
| 63 |
+
check_source(path)
|
| 64 |
+
return
|
| 65 |
+
if depth != 16 or (path / "EXPORT_INCOMPLETE").exists():
|
| 66 |
+
raise ValueError("only complete 16/32 exports accepted")
|
| 67 |
+
manifest = json.loads((path / "export-low-provenance.json").read_text())
|
| 68 |
+
if (
|
| 69 |
+
manifest.get("status") != "verified"
|
| 70 |
+
or manifest.get("actual_depth") != 16
|
| 71 |
+
or manifest.get("all_retained_tensors_bit_identical") is not True
|
| 72 |
+
):
|
| 73 |
+
raise ValueError("low export provenance is not verified")
|
| 74 |
+
for name, field in (
|
| 75 |
+
("config.json", "export_config_sha256"),
|
| 76 |
+
("model.safetensors.index.json", "export_index_sha256"),
|
| 77 |
+
):
|
| 78 |
+
if file_sha(path / name) != manifest[field]:
|
| 79 |
+
raise ValueError("low config/index SHA mismatch")
|
| 80 |
+
weights = json.loads((path / "model.safetensors.index.json").read_text())[
|
| 81 |
+
"weight_map"
|
| 82 |
+
]
|
| 83 |
+
select_keys(weights, 16, 16)
|
| 84 |
+
entries = manifest["export_weight_files"]
|
| 85 |
+
expected = {entry["file"]: entry for entry in entries}
|
| 86 |
+
if len(expected) != len(entries) or set(expected) != set(weights.values()):
|
| 87 |
+
raise ValueError("low weight manifest/index mismatch")
|
| 88 |
+
for name, entry in expected.items():
|
| 89 |
+
if Path(name).name != name or not name.endswith(".safetensors"):
|
| 90 |
+
raise ValueError("invalid low shard path")
|
| 91 |
+
if (path / name).stat().st_size != entry["bytes"] or file_sha(
|
| 92 |
+
path / name
|
| 93 |
+
) != entry["sha256"]:
|
| 94 |
+
raise ValueError(f"low weight SHA mismatch: {name}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def tensor_sha(tensor):
|
| 98 |
+
import torch
|
| 99 |
+
|
| 100 |
+
raw = memoryview(tensor.contiguous().view(torch.uint8).numpy()).cast("B")
|
| 101 |
+
h = hashlib.sha256()
|
| 102 |
+
for start in range(0, len(raw), 8 * 1024 * 1024):
|
| 103 |
+
h.update(raw[start : start + 8 * 1024 * 1024])
|
| 104 |
+
return h.hexdigest()
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def prefix_config(config, depth=16):
|
| 108 |
+
result = copy.deepcopy(config)
|
| 109 |
+
text = result["text_config"]
|
| 110 |
+
if (
|
| 111 |
+
not 0 < depth < text["num_hidden_layers"]
|
| 112 |
+
or len(text["layer_types"]) != text["num_hidden_layers"]
|
| 113 |
+
):
|
| 114 |
+
raise ValueError("invalid depth or layer_types")
|
| 115 |
+
text["num_hidden_layers"] = depth
|
| 116 |
+
text["layer_types"] = text["layer_types"][:depth]
|
| 117 |
+
if "mlp_only_layers" in text:
|
| 118 |
+
text["mlp_only_layers"] = [i for i in text["mlp_only_layers"] if i < depth]
|
| 119 |
+
return result
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def select_keys(keys, depth, full_depth):
|
| 123 |
+
kept, removed, seen = [], [], set()
|
| 124 |
+
for key in sorted(keys):
|
| 125 |
+
match = LAYER.match(key)
|
| 126 |
+
if match:
|
| 127 |
+
i = int(match.group(1))
|
| 128 |
+
if not 0 <= i < full_depth:
|
| 129 |
+
raise ValueError("layer outside full depth")
|
| 130 |
+
seen.add(i)
|
| 131 |
+
(kept if i < depth else removed).append(key)
|
| 132 |
+
elif key in READOUT or key.startswith("model.visual."):
|
| 133 |
+
kept.append(key)
|
| 134 |
+
else:
|
| 135 |
+
raise ValueError(f"unexpected weight namespace: {key}")
|
| 136 |
+
if seen != set(range(full_depth)) or not READOUT <= set(kept):
|
| 137 |
+
raise ValueError("missing decoder, embedding, final norm or lm_head")
|
| 138 |
+
return kept, removed
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def check_source(source):
|
| 142 |
+
config = json.loads((source / "config.json").read_text())
|
| 143 |
+
text = config.get("text_config", {})
|
| 144 |
+
if (
|
| 145 |
+
config.get("model_type") != "qwen3_5"
|
| 146 |
+
or config.get("architectures") != ["Qwen3_5ForConditionalGeneration"]
|
| 147 |
+
or text.get("num_hidden_layers") != 32
|
| 148 |
+
or text.get("hidden_size") != 4096
|
| 149 |
+
or text.get("vocab_size") != 248320
|
| 150 |
+
or text.get("intermediate_size") != 12288
|
| 151 |
+
or config.get("tie_word_embeddings") is not False
|
| 152 |
+
or text.get("tie_word_embeddings") is not False
|
| 153 |
+
or (source / "adapter_config.json").exists()
|
| 154 |
+
):
|
| 155 |
+
raise ValueError("only merged, untied dense Qwen3.5-9B full32 accepted")
|
| 156 |
+
depth = json.loads((source / "depth_config.json").read_text())
|
| 157 |
+
if depth.get("exit_depth") != 16 or depth.get("full_depth") != 32:
|
| 158 |
+
raise ValueError("source must declare trained exits 16/32")
|
| 159 |
+
if text.get("layer_types") != (["linear_attention"] * 3 + ["full_attention"]) * 8:
|
| 160 |
+
raise ValueError("unexpected Qwen3.5-9B hybrid schedule")
|
| 161 |
+
weight_map = json.loads((source / "model.safetensors.index.json").read_text())[
|
| 162 |
+
"weight_map"
|
| 163 |
+
]
|
| 164 |
+
kept, removed = select_keys(weight_map, 16, 32)
|
| 165 |
+
manifest = json.loads((source / "release-manifest.json").read_text())
|
| 166 |
+
expected = {r["path"]: r["sha256"] for r in manifest["files"]}
|
| 167 |
+
source_files = []
|
| 168 |
+
for name in sorted(set(weight_map.values())):
|
| 169 |
+
if Path(name).name != name or not name.endswith(".safetensors"):
|
| 170 |
+
raise ValueError("weight shard must be a simple safetensors filename")
|
| 171 |
+
observed = file_sha(source / name)
|
| 172 |
+
if expected.get(name) != observed:
|
| 173 |
+
raise ValueError(f"source weight SHA mismatch: {name}")
|
| 174 |
+
source_files.append(
|
| 175 |
+
{"file": name, "sha256": observed, "bytes": (source / name).stat().st_size}
|
| 176 |
+
)
|
| 177 |
+
for name in (
|
| 178 |
+
"config.json",
|
| 179 |
+
"depth_config.json",
|
| 180 |
+
"model.safetensors.index.json",
|
| 181 |
+
"tokenizer.json",
|
| 182 |
+
"tokenizer_config.json",
|
| 183 |
+
"chat_template.jinja",
|
| 184 |
+
):
|
| 185 |
+
if name not in expected or file_sha(source / name) != expected[name]:
|
| 186 |
+
raise ValueError(f"source metadata SHA mismatch: {name}")
|
| 187 |
+
return config, weight_map, kept, removed, source_files
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def copy_weights(source, output, weight_map, kept):
|
| 191 |
+
from safetensors import safe_open
|
| 192 |
+
from safetensors.torch import save_file
|
| 193 |
+
|
| 194 |
+
shards = [
|
| 195 |
+
n
|
| 196 |
+
for n in sorted(set(weight_map.values()))
|
| 197 |
+
if any(weight_map[k] == n for k in kept)
|
| 198 |
+
]
|
| 199 |
+
output_map, files, manifests = {}, [], []
|
| 200 |
+
total = 0
|
| 201 |
+
for ordinal, name in enumerate(shards, 1):
|
| 202 |
+
selected = sorted(k for k in kept if weight_map[k] == name)
|
| 203 |
+
with safe_open(source / name, framework="pt", device="cpu") as src:
|
| 204 |
+
if set(src.keys()) != {k for k, v in weight_map.items() if v == name}:
|
| 205 |
+
raise ValueError("source shard/index mismatch")
|
| 206 |
+
tensors = {k: src.get_tensor(k) for k in selected}
|
| 207 |
+
total += sum(t.numel() * t.element_size() for t in tensors.values())
|
| 208 |
+
out_name = f"model-low16-{ordinal:05d}-of-{len(shards):05d}.safetensors"
|
| 209 |
+
save_file(tensors, output / out_name, metadata={"format": "pt"})
|
| 210 |
+
with safe_open(output / out_name, framework="pt", device="cpu") as dst:
|
| 211 |
+
if set(dst.keys()) != set(selected):
|
| 212 |
+
raise ValueError("exported keys mismatch")
|
| 213 |
+
for key in selected:
|
| 214 |
+
original, exported = tensors[key], dst.get_tensor(key)
|
| 215 |
+
checksum = tensor_sha(original)
|
| 216 |
+
if (
|
| 217 |
+
original.dtype != exported.dtype
|
| 218 |
+
or original.shape != exported.shape
|
| 219 |
+
or checksum != tensor_sha(exported)
|
| 220 |
+
):
|
| 221 |
+
raise ValueError(f"exported tensor changed: {key}")
|
| 222 |
+
manifests.append(
|
| 223 |
+
{
|
| 224 |
+
"name": key,
|
| 225 |
+
"shape": list(original.shape),
|
| 226 |
+
"dtype": str(original.dtype),
|
| 227 |
+
"sha256": checksum,
|
| 228 |
+
}
|
| 229 |
+
)
|
| 230 |
+
output_map[key] = out_name
|
| 231 |
+
files.append(
|
| 232 |
+
{
|
| 233 |
+
"file": out_name,
|
| 234 |
+
"sha256": file_sha(output / out_name),
|
| 235 |
+
"bytes": (output / out_name).stat().st_size,
|
| 236 |
+
}
|
| 237 |
+
)
|
| 238 |
+
print(
|
| 239 |
+
json.dumps(
|
| 240 |
+
{"verified_export_shard": out_name, "tensor_count": len(selected)}
|
| 241 |
+
),
|
| 242 |
+
flush=True,
|
| 243 |
+
)
|
| 244 |
+
if set(output_map) != set(kept):
|
| 245 |
+
raise ValueError("export index incomplete")
|
| 246 |
+
(output / "model.safetensors.index.json").write_text(
|
| 247 |
+
json.dumps(
|
| 248 |
+
{"metadata": {"total_size": total}, "weight_map": output_map}, indent=2
|
| 249 |
+
)
|
| 250 |
+
+ "\n"
|
| 251 |
+
)
|
| 252 |
+
return files, manifests
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def export(source, output):
|
| 256 |
+
from safetensors import safe_open
|
| 257 |
+
|
| 258 |
+
source, output = Path(source).resolve(), Path(output).resolve()
|
| 259 |
+
if output.exists() or source in output.parents:
|
| 260 |
+
raise ValueError("output must be a new directory outside source model")
|
| 261 |
+
before = source_snapshot(source)
|
| 262 |
+
config, weight_map, kept, removed, source_files = check_source(source)
|
| 263 |
+
for name in sorted(set(weight_map.values())):
|
| 264 |
+
with safe_open(source / name, framework="pt", device="cpu") as handle:
|
| 265 |
+
if set(handle.keys()) != {k for k, v in weight_map.items() if v == name}:
|
| 266 |
+
raise ValueError("source index/shard mismatch")
|
| 267 |
+
for key, shape in {
|
| 268 |
+
"lm_head.weight": (248320, 4096),
|
| 269 |
+
"model.language_model.embed_tokens.weight": (248320, 4096),
|
| 270 |
+
"model.language_model.norm.weight": (4096,),
|
| 271 |
+
}.items():
|
| 272 |
+
if (
|
| 273 |
+
weight_map[key] == name
|
| 274 |
+
and tuple(handle.get_slice(key).get_shape()) != shape
|
| 275 |
+
):
|
| 276 |
+
raise ValueError(f"unexpected shared readout shape: {key}")
|
| 277 |
+
output.mkdir(parents=True, exist_ok=False)
|
| 278 |
+
(output / "EXPORT_INCOMPLETE").write_text(
|
| 279 |
+
"Only provenance status=verified denotes success.\n"
|
| 280 |
+
)
|
| 281 |
+
(output / "config.json").write_text(
|
| 282 |
+
json.dumps(prefix_config(config), indent=2) + "\n"
|
| 283 |
+
)
|
| 284 |
+
for name in (
|
| 285 |
+
"tokenizer.json",
|
| 286 |
+
"tokenizer_config.json",
|
| 287 |
+
"chat_template.jinja",
|
| 288 |
+
"generation_config.json",
|
| 289 |
+
"special_tokens_map.json",
|
| 290 |
+
"added_tokens.json",
|
| 291 |
+
"vocab.json",
|
| 292 |
+
"merges.txt",
|
| 293 |
+
"preprocessor_config.json",
|
| 294 |
+
"processor_config.json",
|
| 295 |
+
"video_preprocessor_config.json",
|
| 296 |
+
"LICENSE",
|
| 297 |
+
):
|
| 298 |
+
if (source / name).is_file():
|
| 299 |
+
shutil.copy2(source / name, output / name)
|
| 300 |
+
files, tensors = copy_weights(source, output, weight_map, kept)
|
| 301 |
+
provenance = {
|
| 302 |
+
"format": "openjev.vllm-low-prefix.v1",
|
| 303 |
+
"status": "verified",
|
| 304 |
+
"created_utc": datetime.now(timezone.utc).isoformat(),
|
| 305 |
+
"source": str(source),
|
| 306 |
+
"source_full_depth": 32,
|
| 307 |
+
"actual_depth": 16,
|
| 308 |
+
"dynamic_continuation": False,
|
| 309 |
+
"selection": "original decoder 0..15 + shared embed/final norm/lm_head + all visual tensors",
|
| 310 |
+
"source_config_sha256": file_sha(source / "config.json"),
|
| 311 |
+
"source_depth_config": json.loads((source / "depth_config.json").read_text()),
|
| 312 |
+
"source_release_manifest_sha256": file_sha(source / "release-manifest.json"),
|
| 313 |
+
"source_index_sha256": file_sha(source / "model.safetensors.index.json"),
|
| 314 |
+
"source_weight_files": source_files,
|
| 315 |
+
"export_config_sha256": file_sha(output / "config.json"),
|
| 316 |
+
"export_index_sha256": file_sha(output / "model.safetensors.index.json"),
|
| 317 |
+
"export_weight_files": files,
|
| 318 |
+
"retained_tensors": tensors,
|
| 319 |
+
"removed_tensors": removed,
|
| 320 |
+
"all_retained_tensors_bit_identical": True,
|
| 321 |
+
"vllm_numerical_parity": "not established by CPU export; requires independent GPU comparison",
|
| 322 |
+
"script_sha256": file_sha(__file__),
|
| 323 |
+
}
|
| 324 |
+
assert_source_unchanged(source, before)
|
| 325 |
+
provenance["source_files_stat_before_after"] = before
|
| 326 |
+
provenance["source_files_unchanged_during_export"] = True
|
| 327 |
+
(output / "export-low-provenance.json").write_text(
|
| 328 |
+
json.dumps(provenance, indent=2) + "\n"
|
| 329 |
+
)
|
| 330 |
+
(output / "EXPORT_INCOMPLETE").unlink()
|
| 331 |
+
return provenance
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def main():
|
| 335 |
+
p = argparse.ArgumentParser(description=__doc__)
|
| 336 |
+
p.add_argument("--source", type=Path, required=True)
|
| 337 |
+
p.add_argument("--output", type=Path, required=True)
|
| 338 |
+
args = p.parse_args()
|
| 339 |
+
result = export(args.source, args.output)
|
| 340 |
+
print(
|
| 341 |
+
json.dumps(
|
| 342 |
+
{
|
| 343 |
+
"status": result["status"],
|
| 344 |
+
"output": str(args.output),
|
| 345 |
+
"actual_depth": 16,
|
| 346 |
+
"retained_tensors": len(result["retained_tensors"]),
|
| 347 |
+
}
|
| 348 |
+
),
|
| 349 |
+
flush=True,
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
if __name__ == "__main__":
|
| 354 |
+
main()
|
deployment/gateway.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""TypeSafe SDK wire gateway to distinct shallow/full vLLM scoring engines."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import asyncio
|
| 7 |
+
from contextlib import asynccontextmanager
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
import hashlib
|
| 10 |
+
import hmac
|
| 11 |
+
import json
|
| 12 |
+
import math
|
| 13 |
+
import os
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
import re
|
| 16 |
+
import uuid
|
| 17 |
+
|
| 18 |
+
import httpx
|
| 19 |
+
from fastapi import FastAPI, Request
|
| 20 |
+
from fastapi.responses import JSONResponse
|
| 21 |
+
|
| 22 |
+
from contracts import (
|
| 23 |
+
ServingError,
|
| 24 |
+
answer,
|
| 25 |
+
compile_question,
|
| 26 |
+
prepare_request,
|
| 27 |
+
probabilities,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def canonical_root(value):
|
| 32 |
+
url = httpx.URL(value)
|
| 33 |
+
path = url.path.rstrip("/").removesuffix("/v1").rstrip("/")
|
| 34 |
+
return str(url.copy_with(path=path))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@dataclass(frozen=True)
|
| 38 |
+
class Settings:
|
| 39 |
+
high_url: str
|
| 40 |
+
low_url: str
|
| 41 |
+
high_model: str
|
| 42 |
+
low_model: str
|
| 43 |
+
api_key: str
|
| 44 |
+
model_name: str = "APUS-OpenJev-v1-9B"
|
| 45 |
+
max_body_bytes: int = 1_048_576
|
| 46 |
+
max_questions: int = 16
|
| 47 |
+
max_tokens: int = 8191
|
| 48 |
+
max_inflight: int = 32
|
| 49 |
+
timeout: float = 120.0
|
| 50 |
+
high_model_path: str | None = None
|
| 51 |
+
low_model_path: str | None = None
|
| 52 |
+
|
| 53 |
+
def __post_init__(self):
|
| 54 |
+
for value in (self.high_url, self.low_url):
|
| 55 |
+
url = httpx.URL(value)
|
| 56 |
+
if (
|
| 57 |
+
url.scheme not in ("http", "https")
|
| 58 |
+
or not url.host
|
| 59 |
+
or url.username
|
| 60 |
+
or url.password
|
| 61 |
+
or url.query
|
| 62 |
+
or url.fragment
|
| 63 |
+
):
|
| 64 |
+
raise ValueError(
|
| 65 |
+
"Backend URLs must be HTTP(S) without credentials/query"
|
| 66 |
+
)
|
| 67 |
+
if canonical_root(self.high_url) == canonical_root(self.low_url):
|
| 68 |
+
raise ValueError(
|
| 69 |
+
"Low and high must use distinct engine URLs; no silent full-depth fallback"
|
| 70 |
+
)
|
| 71 |
+
if not all((self.high_model, self.low_model, self.api_key, self.model_name)):
|
| 72 |
+
raise ValueError(
|
| 73 |
+
"Both model aliases and a nonempty local API key are required"
|
| 74 |
+
)
|
| 75 |
+
if self.high_model == self.low_model:
|
| 76 |
+
raise ValueError("Low and high engine model aliases must be distinct")
|
| 77 |
+
|
| 78 |
+
@classmethod
|
| 79 |
+
def from_env(cls):
|
| 80 |
+
return cls(
|
| 81 |
+
high_url=os.environ["JEV_HIGH_URL"],
|
| 82 |
+
low_url=os.environ["JEV_LOW_URL"],
|
| 83 |
+
high_model=os.environ["JEV_HIGH_MODEL"],
|
| 84 |
+
low_model=os.environ["JEV_LOW_MODEL"],
|
| 85 |
+
api_key=os.environ["JEV_API_KEY"],
|
| 86 |
+
high_model_path=os.environ["JEV_HIGH_MODEL_PATH"],
|
| 87 |
+
low_model_path=os.environ["JEV_LOW_MODEL_PATH"],
|
| 88 |
+
model_name=os.environ.get("JEV_MODEL_NAME", "APUS-OpenJev-v1-9B"),
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def unique_object(pairs):
|
| 93 |
+
result = {}
|
| 94 |
+
for key, value in pairs:
|
| 95 |
+
if key in result:
|
| 96 |
+
raise ValueError("Duplicate JSON keys")
|
| 97 |
+
result[key] = value
|
| 98 |
+
return result
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def reject_constant(value):
|
| 102 |
+
raise ValueError("Nonfinite JSON values are forbidden")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def parse_scores(data, tokens):
|
| 106 |
+
try:
|
| 107 |
+
choices = data["choices"]
|
| 108 |
+
if len(choices) != 1 or len(choices[0]["logprobs"]["top_logprobs"]) != 1:
|
| 109 |
+
raise ValueError
|
| 110 |
+
values = choices[0]["logprobs"]["top_logprobs"][0]
|
| 111 |
+
if not isinstance(values, dict):
|
| 112 |
+
raise ValueError
|
| 113 |
+
by_id = {}
|
| 114 |
+
for key, value in values.items():
|
| 115 |
+
if (
|
| 116 |
+
not re.fullmatch(r"token_id:(0|[1-9]\d*)", key)
|
| 117 |
+
or type(value) not in (float, int)
|
| 118 |
+
or not math.isfinite(value)
|
| 119 |
+
or value > 0
|
| 120 |
+
):
|
| 121 |
+
raise ValueError
|
| 122 |
+
by_id[int(key.split(":")[1])] = value
|
| 123 |
+
return [by_id[token] for token in tokens]
|
| 124 |
+
except (KeyError, TypeError, ValueError, IndexError):
|
| 125 |
+
raise ServingError(
|
| 126 |
+
502,
|
| 127 |
+
"invalid_backend_scores",
|
| 128 |
+
"Backend omitted valid candidate token logprobs",
|
| 129 |
+
) from None
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def endpoint(url, path):
|
| 133 |
+
root = url.rstrip("/")
|
| 134 |
+
return root + (path.removeprefix("/v1") if root.endswith("/v1") else path)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def file_sha(path):
|
| 138 |
+
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def verify_artifacts(settings):
|
| 142 |
+
"""Bind configured backend roots to real model configs and export identity."""
|
| 143 |
+
if not settings.high_model_path or not settings.low_model_path:
|
| 144 |
+
raise ValueError("Both JEV_HIGH_MODEL_PATH and JEV_LOW_MODEL_PATH are required")
|
| 145 |
+
roots = {
|
| 146 |
+
"high": Path(settings.high_model_path).resolve(),
|
| 147 |
+
"low": Path(settings.low_model_path).resolve(),
|
| 148 |
+
}
|
| 149 |
+
for effort, depth in (("high", 32), ("low", 16)):
|
| 150 |
+
root = roots[effort]
|
| 151 |
+
cfg = json.loads((root / "config.json").read_text())
|
| 152 |
+
text = cfg.get("text_config", cfg)
|
| 153 |
+
if (
|
| 154 |
+
cfg.get("model_type") not in ("qwen3_5", "qwen3_5_text")
|
| 155 |
+
or text.get("num_hidden_layers") != depth
|
| 156 |
+
or len(text.get("layer_types", [])) != depth
|
| 157 |
+
or text.get("hidden_size") != 4096
|
| 158 |
+
):
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"Backend artifact does not declare the required execution depth"
|
| 161 |
+
)
|
| 162 |
+
if (root / "adapter_config.json").exists() or (
|
| 163 |
+
root / "EXPORT_INCOMPLETE"
|
| 164 |
+
).exists():
|
| 165 |
+
raise ValueError("Backend artifact is not a completed merged model")
|
| 166 |
+
if not (root / "model.safetensors.index.json").is_file():
|
| 167 |
+
raise ValueError("Backend artifact has no merged weight index")
|
| 168 |
+
low, high = roots["low"], roots["high"]
|
| 169 |
+
proof = json.loads((low / "export-low-provenance.json").read_text())
|
| 170 |
+
if (
|
| 171 |
+
proof.get("status") != "verified"
|
| 172 |
+
or proof.get("actual_depth") != 16
|
| 173 |
+
or proof.get("all_retained_tensors_bit_identical") is not True
|
| 174 |
+
):
|
| 175 |
+
raise ValueError("Low artifact has not passed prefix export verification")
|
| 176 |
+
for key, path in {
|
| 177 |
+
"export_config_sha256": low / "config.json",
|
| 178 |
+
"export_index_sha256": low / "model.safetensors.index.json",
|
| 179 |
+
"source_config_sha256": high / "config.json",
|
| 180 |
+
"source_index_sha256": high / "model.safetensors.index.json",
|
| 181 |
+
"source_release_manifest_sha256": high / "release-manifest.json",
|
| 182 |
+
}.items():
|
| 183 |
+
if proof.get(key) != file_sha(path):
|
| 184 |
+
raise ValueError(
|
| 185 |
+
"Low export provenance does not match the configured source or export metadata"
|
| 186 |
+
)
|
| 187 |
+
return roots
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def create_app(settings, tokenizer, transport=None, check_backends=True):
|
| 191 |
+
@asynccontextmanager
|
| 192 |
+
async def lifespan(app):
|
| 193 |
+
async with httpx.AsyncClient(
|
| 194 |
+
timeout=settings.timeout,
|
| 195 |
+
trust_env=False,
|
| 196 |
+
follow_redirects=False,
|
| 197 |
+
transport=transport,
|
| 198 |
+
) as client:
|
| 199 |
+
app.state.client = client
|
| 200 |
+
if check_backends:
|
| 201 |
+
roots = verify_artifacts(settings)
|
| 202 |
+
for effort, url, alias in (
|
| 203 |
+
("low", settings.low_url, settings.low_model),
|
| 204 |
+
("high", settings.high_url, settings.high_model),
|
| 205 |
+
):
|
| 206 |
+
response = await client.get(endpoint(url, "/v1/models"))
|
| 207 |
+
response.raise_for_status()
|
| 208 |
+
matched = [m for m in response.json()["data"] if m["id"] == alias]
|
| 209 |
+
if (
|
| 210 |
+
len(matched) != 1
|
| 211 |
+
or not matched[0].get("root")
|
| 212 |
+
or Path(matched[0]["root"]).resolve() != roots[effort]
|
| 213 |
+
):
|
| 214 |
+
raise RuntimeError(
|
| 215 |
+
"Engine alias/root does not match the verified depth artifact"
|
| 216 |
+
)
|
| 217 |
+
app.state.ready = True
|
| 218 |
+
yield
|
| 219 |
+
app.state.ready = False
|
| 220 |
+
|
| 221 |
+
app = FastAPI(lifespan=lifespan)
|
| 222 |
+
app.state.ready = False
|
| 223 |
+
app.state.inflight = 0
|
| 224 |
+
|
| 225 |
+
def authorize(request):
|
| 226 |
+
authorization = request.headers.get("authorization", "")
|
| 227 |
+
if not hmac.compare_digest(
|
| 228 |
+
authorization.encode(), ("Bearer " + settings.api_key).encode()
|
| 229 |
+
):
|
| 230 |
+
raise ServingError(401, "unauthorized", "Invalid local API key")
|
| 231 |
+
|
| 232 |
+
@app.middleware("http")
|
| 233 |
+
async def request_id_header(request, call_next):
|
| 234 |
+
response = await call_next(request)
|
| 235 |
+
response.headers.setdefault("x-typesafe-request-id", str(uuid.uuid4()))
|
| 236 |
+
return response
|
| 237 |
+
|
| 238 |
+
@app.exception_handler(ServingError)
|
| 239 |
+
async def serving_error(_request, exc):
|
| 240 |
+
return JSONResponse(
|
| 241 |
+
status_code=exc.status,
|
| 242 |
+
content={"error": {"code": exc.code, "message": exc.message}},
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
@app.get("/health")
|
| 246 |
+
async def health():
|
| 247 |
+
return JSONResponse(
|
| 248 |
+
status_code=200 if app.state.ready else 503,
|
| 249 |
+
content={
|
| 250 |
+
"ready": app.state.ready,
|
| 251 |
+
"model": settings.model_name,
|
| 252 |
+
"efforts": {"low": 16, "high": 32},
|
| 253 |
+
"routing": "separate-vllm-engines",
|
| 254 |
+
"automatic_routing": False,
|
| 255 |
+
},
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
@app.get("/v1/models")
|
| 259 |
+
async def models(request: Request):
|
| 260 |
+
authorize(request)
|
| 261 |
+
return {
|
| 262 |
+
"models": [
|
| 263 |
+
{"name": name, "description": description, "release_date": "2026-09-21"}
|
| 264 |
+
for name, description in [
|
| 265 |
+
(
|
| 266 |
+
settings.model_name,
|
| 267 |
+
"APUS local decision model; explicit low16/high32; uncalibrated candidate scores.",
|
| 268 |
+
),
|
| 269 |
+
(
|
| 270 |
+
"jev-latest",
|
| 271 |
+
"Compatibility alias for "
|
| 272 |
+
+ settings.model_name
|
| 273 |
+
+ "; NOT the official Jev model.",
|
| 274 |
+
),
|
| 275 |
+
]
|
| 276 |
+
]
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
@app.post("/v1/systemone")
|
| 280 |
+
async def systemone(request: Request):
|
| 281 |
+
authorize(request)
|
| 282 |
+
if not app.state.ready:
|
| 283 |
+
raise ServingError(503, "not_ready", "Backends are not ready")
|
| 284 |
+
if app.state.inflight >= settings.max_inflight:
|
| 285 |
+
raise ServingError(429, "busy", "Gateway in-flight budget exceeded")
|
| 286 |
+
app.state.inflight += 1
|
| 287 |
+
try:
|
| 288 |
+
async with asyncio.timeout(settings.timeout):
|
| 289 |
+
raw = bytearray()
|
| 290 |
+
async for chunk in request.stream():
|
| 291 |
+
raw.extend(chunk)
|
| 292 |
+
if len(raw) > settings.max_body_bytes:
|
| 293 |
+
raise ServingError(
|
| 294 |
+
413, "request_too_large", "Request byte budget exceeded"
|
| 295 |
+
)
|
| 296 |
+
try:
|
| 297 |
+
body = json.loads(
|
| 298 |
+
raw,
|
| 299 |
+
object_pairs_hook=unique_object,
|
| 300 |
+
parse_constant=reject_constant,
|
| 301 |
+
)
|
| 302 |
+
if not isinstance(body, dict):
|
| 303 |
+
raise ValueError
|
| 304 |
+
except (ValueError, TypeError, RecursionError, UnicodeError):
|
| 305 |
+
raise ServingError(
|
| 306 |
+
422, "invalid_json", "Malformed JSON object"
|
| 307 |
+
) from None
|
| 308 |
+
effort = body.get("effort", "high")
|
| 309 |
+
if effort not in ("low", "high"):
|
| 310 |
+
raise ServingError(
|
| 311 |
+
422, "invalid_effort", "effort must be low or high"
|
| 312 |
+
)
|
| 313 |
+
questions = prepare_request(
|
| 314 |
+
body,
|
| 315 |
+
model_name=settings.model_name,
|
| 316 |
+
max_questions=settings.max_questions,
|
| 317 |
+
allow_score=True,
|
| 318 |
+
)
|
| 319 |
+
compiled = [
|
| 320 |
+
compile_question(q, tokenizer, max_tokens=settings.max_tokens)
|
| 321 |
+
for q in questions
|
| 322 |
+
]
|
| 323 |
+
url, model, depth = (
|
| 324 |
+
(settings.low_url, settings.low_model, 16)
|
| 325 |
+
if effort == "low"
|
| 326 |
+
else (settings.high_url, settings.high_model, 32)
|
| 327 |
+
)
|
| 328 |
+
answers = {}
|
| 329 |
+
for item in compiled:
|
| 330 |
+
if not item.prompt_ids:
|
| 331 |
+
answers[item.question.key] = answer(item.question, [1.0])
|
| 332 |
+
continue
|
| 333 |
+
payload = {
|
| 334 |
+
"model": model,
|
| 335 |
+
"prompt": item.prompt_ids,
|
| 336 |
+
"max_tokens": 1,
|
| 337 |
+
"n": 1,
|
| 338 |
+
"stream": False,
|
| 339 |
+
"echo": False,
|
| 340 |
+
"add_special_tokens": False,
|
| 341 |
+
"logprobs": len(item.candidate_token_ids),
|
| 342 |
+
"logprob_token_ids": item.candidate_token_ids,
|
| 343 |
+
"return_tokens_as_token_ids": True,
|
| 344 |
+
"temperature": 1.0,
|
| 345 |
+
"top_p": 1.0,
|
| 346 |
+
"top_k": -1,
|
| 347 |
+
"min_p": 0.0,
|
| 348 |
+
"frequency_penalty": 0.0,
|
| 349 |
+
"presence_penalty": 0.0,
|
| 350 |
+
"repetition_penalty": 1.0,
|
| 351 |
+
}
|
| 352 |
+
response = await app.state.client.post(
|
| 353 |
+
endpoint(url, "/v1/completions"), json=payload
|
| 354 |
+
)
|
| 355 |
+
if response.status_code != 200:
|
| 356 |
+
raise ServingError(
|
| 357 |
+
502,
|
| 358 |
+
"backend_error",
|
| 359 |
+
f"Inference backend returned HTTP {response.status_code}",
|
| 360 |
+
)
|
| 361 |
+
try:
|
| 362 |
+
data = json.loads(
|
| 363 |
+
response.content,
|
| 364 |
+
object_pairs_hook=unique_object,
|
| 365 |
+
parse_constant=reject_constant,
|
| 366 |
+
)
|
| 367 |
+
except (ValueError, TypeError, UnicodeError, RecursionError):
|
| 368 |
+
raise ServingError(
|
| 369 |
+
502, "backend_json", "Backend returned invalid JSON"
|
| 370 |
+
) from None
|
| 371 |
+
answers[item.question.key] = answer(
|
| 372 |
+
item.question,
|
| 373 |
+
probabilities(parse_scores(data, item.candidate_token_ids)),
|
| 374 |
+
)
|
| 375 |
+
result = {
|
| 376 |
+
"model": settings.model_name,
|
| 377 |
+
"answers": answers,
|
| 378 |
+
"usage": {
|
| 379 |
+
"input_tokens": sum(len(c.prompt_ids) for c in compiled),
|
| 380 |
+
"output_tokens": sum(bool(c.prompt_ids) for c in compiled),
|
| 381 |
+
"billing_units": len(compiled),
|
| 382 |
+
},
|
| 383 |
+
}
|
| 384 |
+
return JSONResponse(
|
| 385 |
+
result,
|
| 386 |
+
headers={
|
| 387 |
+
"x-openjev-effort": effort,
|
| 388 |
+
"x-openjev-executed-layers": str(
|
| 389 |
+
depth if any(c.prompt_ids for c in compiled) else 0
|
| 390 |
+
),
|
| 391 |
+
"x-openjev-engine-model": model,
|
| 392 |
+
"x-typesafe-request-id": str(uuid.uuid4()),
|
| 393 |
+
},
|
| 394 |
+
)
|
| 395 |
+
except (TimeoutError, httpx.TimeoutException):
|
| 396 |
+
raise ServingError(
|
| 397 |
+
504, "deadline_exceeded", "Inference request exceeded timeout"
|
| 398 |
+
) from None
|
| 399 |
+
except httpx.HTTPError:
|
| 400 |
+
raise ServingError(
|
| 401 |
+
502, "backend_transport", "Inference backend connection failed"
|
| 402 |
+
) from None
|
| 403 |
+
finally:
|
| 404 |
+
app.state.inflight -= 1
|
| 405 |
+
|
| 406 |
+
return app
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def main():
|
| 410 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 411 |
+
parser.add_argument("--host", default="127.0.0.1")
|
| 412 |
+
parser.add_argument("--port", type=int, default=30013)
|
| 413 |
+
args = parser.parse_args()
|
| 414 |
+
from transformers import AutoTokenizer
|
| 415 |
+
import uvicorn
|
| 416 |
+
|
| 417 |
+
settings = Settings.from_env()
|
| 418 |
+
if (
|
| 419 |
+
Path(os.environ["JEV_MODEL_PATH"]).resolve()
|
| 420 |
+
!= Path(settings.high_model_path).resolve()
|
| 421 |
+
):
|
| 422 |
+
raise ValueError("Tokenizer must come from the configured full-depth model")
|
| 423 |
+
tokenizer = AutoTokenizer.from_pretrained(
|
| 424 |
+
os.environ["JEV_MODEL_PATH"], local_files_only=True
|
| 425 |
+
)
|
| 426 |
+
uvicorn.run(create_app(settings, tokenizer), host=args.host, port=args.port)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
if __name__ == "__main__":
|
| 430 |
+
main()
|
deployment/requirements-test.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CPU SDK/gateway tests can run without installing vLLM or PyTorch.
|
| 2 |
+
fastapi==0.128.8
|
| 3 |
+
uvicorn==0.40.0
|
| 4 |
+
httpx==0.28.1
|
| 5 |
+
httpx2==2.13.0
|
| 6 |
+
typesafe-sdk==0.7.0
|
| 7 |
+
pytest==9.0.2
|
deployment/requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# vLLM owns the CUDA PyTorch dependency; use a compatible CUDA 13 compiler on Blackwell.
|
| 2 |
+
vllm==0.29.0
|
| 3 |
+
transformers==5.17.0
|
| 4 |
+
huggingface-hub==1.32.0
|
| 5 |
+
ninja==1.13.2
|
| 6 |
+
fastapi==0.128.8
|
| 7 |
+
uvicorn==0.40.0
|
| 8 |
+
httpx==0.28.1
|
| 9 |
+
httpx2==2.13.0
|
| 10 |
+
typesafe-sdk==0.7.0
|
deployment/run_backend.sh
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Launch one dedicated depth backend; never stops another process.
|
| 3 |
+
set -euo pipefail
|
| 4 |
+
BUNDLE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
| 5 |
+
: "${JEV_VLLM_PYTHON:?Absolute Python path in the vLLM 0.29.0 venv}"
|
| 6 |
+
: "${JEV_MODEL_PATH:?Path to full32 merged model or verified exported low16}"
|
| 7 |
+
: "${CUDA_VISIBLE_DEVICES:?Choose exactly one GPU UUID or index}"
|
| 8 |
+
JEV_EFFORT=${JEV_EFFORT:-low}
|
| 9 |
+
case "$JEV_EFFORT" in
|
| 10 |
+
low) DEPTH=16; DEFAULT_PORT=30004; DEFAULT_ALIAS=apus-openjev-9b-low16; DEFAULT_MEMORY=0.22 ;;
|
| 11 |
+
high) DEPTH=32; DEFAULT_PORT=30002; DEFAULT_ALIAS=apus-openjev-9b-high32; DEFAULT_MEMORY=0.7 ;;
|
| 12 |
+
*) echo 'JEV_EFFORT must be high or low' >&2; exit 1 ;;
|
| 13 |
+
esac
|
| 14 |
+
JEV_ENGINE_PORT=${JEV_ENGINE_PORT:-$DEFAULT_PORT}
|
| 15 |
+
JEV_MODEL_ALIAS=${JEV_MODEL_ALIAS:-$DEFAULT_ALIAS}
|
| 16 |
+
JEV_GPU_MEMORY_UTILIZATION=${JEV_GPU_MEMORY_UTILIZATION:-$DEFAULT_MEMORY}
|
| 17 |
+
JEV_MAX_MODEL_LEN=${JEV_MAX_MODEL_LEN:-8192}
|
| 18 |
+
JEV_MAX_NUM_SEQS=${JEV_MAX_NUM_SEQS:-8}
|
| 19 |
+
[[ "$JEV_VLLM_PYTHON" = /* && -x "$JEV_VLLM_PYTHON" ]] || exit 1
|
| 20 |
+
[[ "$CUDA_VISIBLE_DEVICES" =~ ^([0-9]+|GPU-[a-fA-F0-9-]+)$ ]] || exit 1
|
| 21 |
+
export PATH="$(dirname -- "$JEV_VLLM_PYTHON"):$PATH"
|
| 22 |
+
if [[ -n "${CUDA_HOME:-}" ]]; then
|
| 23 |
+
[[ -x "$CUDA_HOME/bin/nvcc" ]] || { echo 'Invalid CUDA_HOME/bin/nvcc' >&2; exit 1; }
|
| 24 |
+
export PATH="$CUDA_HOME/bin:$PATH"
|
| 25 |
+
fi
|
| 26 |
+
# This checks the target port and artifact only. Co-residence with high is intentional.
|
| 27 |
+
"$JEV_VLLM_PYTHON" - "$JEV_MODEL_PATH" "$DEPTH" "$JEV_ENGINE_PORT" "$BUNDLE" <<'PY'
|
| 28 |
+
import importlib.metadata,json,socket,sys
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
p=Path(sys.argv[1]); depth=int(sys.argv[2]); port=int(sys.argv[3])
|
| 31 |
+
assert importlib.metadata.version('vllm')=='0.29.0', 'vLLM must be 0.29.0'
|
| 32 |
+
sys.path.insert(0,sys.argv[4])
|
| 33 |
+
from export_low import verify_artifact
|
| 34 |
+
verify_artifact(p,depth)
|
| 35 |
+
with socket.socket() as s:
|
| 36 |
+
s.bind(('127.0.0.1',port))
|
| 37 |
+
print(json.dumps({'model':str(p),'actual_depth':depth,'port':port,'dynamic_continuation':False}),flush=True)
|
| 38 |
+
PY
|
| 39 |
+
export HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 TOKENIZERS_PARALLELISM=false
|
| 40 |
+
export VLLM_USE_FLASHINFER_SAMPLER=0
|
| 41 |
+
export OMP_NUM_THREADS=${OMP_NUM_THREADS:-8}
|
| 42 |
+
exec "$JEV_VLLM_PYTHON" "$(dirname -- "$JEV_VLLM_PYTHON")/vllm" serve "$JEV_MODEL_PATH" \
|
| 43 |
+
--served-model-name "$JEV_MODEL_ALIAS" --host 127.0.0.1 --port "$JEV_ENGINE_PORT" \
|
| 44 |
+
--dtype bfloat16 --tensor-parallel-size 1 --max-model-len "$JEV_MAX_MODEL_LEN" \
|
| 45 |
+
--language-model-only --gpu-memory-utilization "$JEV_GPU_MEMORY_UTILIZATION" \
|
| 46 |
+
--max-num-seqs "$JEV_MAX_NUM_SEQS" --max-num-batched-tokens "$JEV_MAX_MODEL_LEN" \
|
| 47 |
+
--max-logprobs 256 --logprobs-mode raw_logprobs --generation-config vllm \
|
| 48 |
+
--no-enable-prefix-caching
|
deployment/sdk_example.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""English customer-support examples using the official TypeSafe Python SDK."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
from typesafe_sdk import Choice, Noul, RetryPolicy, Score, TypeSafeClient
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 11 |
+
parser.add_argument("--base-url", default="http://127.0.0.1:30013")
|
| 12 |
+
parser.add_argument("--model", default="APUS-OpenJev-v1-9B")
|
| 13 |
+
parser.add_argument("--effort", choices=["low", "high"], default="high")
|
| 14 |
+
args = parser.parse_args()
|
| 15 |
+
with TypeSafeClient(
|
| 16 |
+
base_url=args.base_url,
|
| 17 |
+
api_key=os.environ["JEV_API_KEY"],
|
| 18 |
+
model=args.model,
|
| 19 |
+
retry=RetryPolicy(max_retries=0),
|
| 20 |
+
) as client:
|
| 21 |
+
print(client.models.list().model_dump_json(indent=2))
|
| 22 |
+
result = client.system_one(
|
| 23 |
+
state={
|
| 24 |
+
"customer_message": "My order arrived damaged yesterday. I would like a refund.",
|
| 25 |
+
"policy": "A damaged item reported within 30 days is eligible for a refund.",
|
| 26 |
+
},
|
| 27 |
+
questions={
|
| 28 |
+
"support_queue": Choice(
|
| 29 |
+
instructions="Choose the team that should handle this ticket.",
|
| 30 |
+
criteria={
|
| 31 |
+
"returns": "Damaged items, returns, and refunds",
|
| 32 |
+
"billing": "Invoice or payment issues",
|
| 33 |
+
"shipping": "Tracking and delivery updates",
|
| 34 |
+
},
|
| 35 |
+
),
|
| 36 |
+
"refund_eligible": Noul(
|
| 37 |
+
instructions="Does the stated policy make this customer eligible for a refund?"
|
| 38 |
+
),
|
| 39 |
+
"urgency": Score(
|
| 40 |
+
instructions="Rate the urgency of this support ticket.",
|
| 41 |
+
criteria=[
|
| 42 |
+
"Routine question",
|
| 43 |
+
"Customer needs timely assistance",
|
| 44 |
+
"Immediate safety or security risk",
|
| 45 |
+
],
|
| 46 |
+
),
|
| 47 |
+
},
|
| 48 |
+
extra_body={"effort": args.effort},
|
| 49 |
+
)
|
| 50 |
+
print(result.model_dump_json(indent=2))
|
| 51 |
+
print("Effort:", result.raw_http_response.headers.get("x-openjev-effort"))
|
| 52 |
+
print(
|
| 53 |
+
"Configured execution layers:",
|
| 54 |
+
result.raw_http_response.headers.get("x-openjev-executed-layers"),
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
if __name__ == "__main__":
|
| 59 |
+
main()
|
deployment/smoke_sdk.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Live official-SDK protocol smoke; writes requests/responses, never auth headers."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import asyncio
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
import importlib.metadata
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
from typesafe_sdk import (
|
| 13 |
+
AsyncTypeSafeClient,
|
| 14 |
+
Choice,
|
| 15 |
+
Noul,
|
| 16 |
+
RetryPolicy,
|
| 17 |
+
Score,
|
| 18 |
+
TypeSafeClient,
|
| 19 |
+
TypeSafeAPIError,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def now():
|
| 24 |
+
return datetime.now(timezone.utc).isoformat()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def main():
|
| 28 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 29 |
+
parser.add_argument("--base-url", default="http://127.0.0.1:30013")
|
| 30 |
+
parser.add_argument("--model", default="APUS-OpenJev-v1-9B")
|
| 31 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 32 |
+
args = parser.parse_args()
|
| 33 |
+
args.output.mkdir(parents=True, exist_ok=False)
|
| 34 |
+
common = {
|
| 35 |
+
"base_url": args.base_url,
|
| 36 |
+
"api_key": os.environ["JEV_API_KEY"],
|
| 37 |
+
"model": args.model,
|
| 38 |
+
"retry": RetryPolicy(max_retries=0),
|
| 39 |
+
"timeout": 120,
|
| 40 |
+
}
|
| 41 |
+
rows = []
|
| 42 |
+
|
| 43 |
+
def record(name, started, timer, response, request):
|
| 44 |
+
raw = response.raw_http_response
|
| 45 |
+
row = {
|
| 46 |
+
"case": name,
|
| 47 |
+
"started_at": started,
|
| 48 |
+
"finished_at": now(),
|
| 49 |
+
"elapsed_ms": (time.perf_counter() - timer) * 1000,
|
| 50 |
+
"request": json.loads(raw.request.content)
|
| 51 |
+
if raw.request.content
|
| 52 |
+
else request,
|
| 53 |
+
"status_code": raw.status_code,
|
| 54 |
+
"response_text": raw.text,
|
| 55 |
+
"response_headers": {
|
| 56 |
+
k: v
|
| 57 |
+
for k, v in raw.headers.items()
|
| 58 |
+
if k.lower()
|
| 59 |
+
in (
|
| 60 |
+
"x-openjev-effort",
|
| 61 |
+
"x-openjev-executed-layers",
|
| 62 |
+
"x-openjev-engine-model",
|
| 63 |
+
"x-typesafe-request-id",
|
| 64 |
+
"content-type",
|
| 65 |
+
)
|
| 66 |
+
},
|
| 67 |
+
}
|
| 68 |
+
rows.append(row)
|
| 69 |
+
with (args.output / "requests.jsonl").open("a") as out:
|
| 70 |
+
out.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 71 |
+
|
| 72 |
+
state = {
|
| 73 |
+
"message": "My order arrived damaged yesterday. I would like a refund.",
|
| 74 |
+
"policy": "Damaged goods reported within 30 days qualify for a refund.",
|
| 75 |
+
}
|
| 76 |
+
questions = {
|
| 77 |
+
"queue": Choice(
|
| 78 |
+
instructions="Choose the support team.",
|
| 79 |
+
criteria={"returns": "Damaged items and refunds", "billing": None},
|
| 80 |
+
),
|
| 81 |
+
"eligible": Noul(
|
| 82 |
+
instructions="Is this customer eligible under the stated policy?"
|
| 83 |
+
),
|
| 84 |
+
"urgency": Score(
|
| 85 |
+
instructions="Rate support urgency.",
|
| 86 |
+
criteria=[
|
| 87 |
+
"Routine",
|
| 88 |
+
{"meaning": "Timely attention required"},
|
| 89 |
+
["Immediate safety risk"],
|
| 90 |
+
],
|
| 91 |
+
),
|
| 92 |
+
}
|
| 93 |
+
with TypeSafeClient(**common) as sdk:
|
| 94 |
+
t = time.perf_counter()
|
| 95 |
+
started = now()
|
| 96 |
+
result = sdk.models.list()
|
| 97 |
+
record("models", started, t, result, {"method": "GET", "path": "/v1/models"})
|
| 98 |
+
assert any(m.name == args.model for m in result.models)
|
| 99 |
+
for effort, depth in [("high", "32"), ("low", "16")]:
|
| 100 |
+
t = time.perf_counter()
|
| 101 |
+
started = now()
|
| 102 |
+
result = sdk.system_one(
|
| 103 |
+
state=state, questions=questions, extra_body={"effort": effort}
|
| 104 |
+
)
|
| 105 |
+
record(
|
| 106 |
+
"multi-" + effort,
|
| 107 |
+
started,
|
| 108 |
+
t,
|
| 109 |
+
result,
|
| 110 |
+
{
|
| 111 |
+
"state": state,
|
| 112 |
+
"questions": questions,
|
| 113 |
+
"model": args.model,
|
| 114 |
+
"effort": effort,
|
| 115 |
+
},
|
| 116 |
+
)
|
| 117 |
+
assert (
|
| 118 |
+
result.raw_http_response.headers["x-openjev-executed-layers"] == depth
|
| 119 |
+
)
|
| 120 |
+
assert set(result.answers) == set(questions)
|
| 121 |
+
assert result.answers["queue"].choice in ("returns", "billing")
|
| 122 |
+
assert 0 <= result.answers["eligible"].noul <= 1
|
| 123 |
+
assert result.answers["urgency"].legend[1] == {
|
| 124 |
+
"meaning": "Timely attention required"
|
| 125 |
+
}
|
| 126 |
+
assert (
|
| 127 |
+
abs(
|
| 128 |
+
result.answers["urgency"].score
|
| 129 |
+
- sum(
|
| 130 |
+
k * v
|
| 131 |
+
for k, v in result.answers["urgency"].probabilities.items()
|
| 132 |
+
)
|
| 133 |
+
)
|
| 134 |
+
< 1e-6
|
| 135 |
+
)
|
| 136 |
+
t = time.perf_counter()
|
| 137 |
+
started = now()
|
| 138 |
+
try:
|
| 139 |
+
sdk.system_one(
|
| 140 |
+
state="Support ticket.",
|
| 141 |
+
questions={"q": Noul()},
|
| 142 |
+
extra_body={"effort": "invalid"},
|
| 143 |
+
)
|
| 144 |
+
except TypeSafeAPIError as exc:
|
| 145 |
+
status = exc.status
|
| 146 |
+
error_body = exc.body
|
| 147 |
+
row = {
|
| 148 |
+
"case": "invalid-effort",
|
| 149 |
+
"started_at": started,
|
| 150 |
+
"finished_at": now(),
|
| 151 |
+
"elapsed_ms": (time.perf_counter() - t) * 1000,
|
| 152 |
+
"request": {
|
| 153 |
+
"state": "Support ticket.",
|
| 154 |
+
"questions": {"q": {"type": "noul"}},
|
| 155 |
+
"effort": "invalid",
|
| 156 |
+
},
|
| 157 |
+
"status_code": status,
|
| 158 |
+
"response_text": json.dumps(error_body),
|
| 159 |
+
}
|
| 160 |
+
rows.append(row)
|
| 161 |
+
with (args.output / "requests.jsonl").open("a") as out:
|
| 162 |
+
out.write(json.dumps(row) + "\n")
|
| 163 |
+
assert status == 422
|
| 164 |
+
else:
|
| 165 |
+
raise AssertionError("Invalid effort was silently accepted")
|
| 166 |
+
|
| 167 |
+
async def run_async():
|
| 168 |
+
async with AsyncTypeSafeClient(**common) as sdk:
|
| 169 |
+
t = time.perf_counter()
|
| 170 |
+
started = now()
|
| 171 |
+
result = await sdk.system_one(
|
| 172 |
+
state="The customer requests a refund.",
|
| 173 |
+
questions={"refund": Noul(instructions="Is this a refund request?")},
|
| 174 |
+
extra_body={"effort": "low"},
|
| 175 |
+
)
|
| 176 |
+
record(
|
| 177 |
+
"async-low",
|
| 178 |
+
started,
|
| 179 |
+
t,
|
| 180 |
+
result,
|
| 181 |
+
{
|
| 182 |
+
"state": "The customer requests a refund.",
|
| 183 |
+
"questions": {
|
| 184 |
+
"refund": {
|
| 185 |
+
"type": "noul",
|
| 186 |
+
"instructions": "Is this a refund request?",
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"effort": "low",
|
| 190 |
+
},
|
| 191 |
+
)
|
| 192 |
+
assert result.raw_http_response.headers["x-openjev-executed-layers"] == "16"
|
| 193 |
+
|
| 194 |
+
asyncio.run(run_async())
|
| 195 |
+
summary = {
|
| 196 |
+
"status": "passed",
|
| 197 |
+
"sdk_version": importlib.metadata.version("typesafe-sdk"),
|
| 198 |
+
"base_url": args.base_url,
|
| 199 |
+
"model": args.model,
|
| 200 |
+
"completed_at": now(),
|
| 201 |
+
"cases": len(rows),
|
| 202 |
+
"scope": "SDK wire/routing smoke, not model-quality or performance acceptance",
|
| 203 |
+
}
|
| 204 |
+
(args.output / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
|
| 205 |
+
print(json.dumps(summary))
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
deployment/tests/test_typesafe_gateway.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Official SDK 0.7.0 through real ASGI gateway, with deterministic backend."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
import httpx
|
| 9 |
+
import httpx2
|
| 10 |
+
import pytest
|
| 11 |
+
from fastapi.testclient import TestClient
|
| 12 |
+
from typesafe_sdk import (
|
| 13 |
+
AsyncTypeSafeClient,
|
| 14 |
+
Choice,
|
| 15 |
+
Noul,
|
| 16 |
+
Score,
|
| 17 |
+
TypeSafeClient,
|
| 18 |
+
RetryPolicy,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 22 |
+
from gateway import Settings, create_app # noqa: E402
|
| 23 |
+
from contracts import BINARY_CRITERIA, prepare_request, compile_question # noqa: E402
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class Tokenizer:
|
| 27 |
+
all_special_ids = [0]
|
| 28 |
+
|
| 29 |
+
def apply_chat_template(self, messages, **kwargs):
|
| 30 |
+
assert kwargs["enable_thinking"] is False
|
| 31 |
+
return "<chat>" + messages[0]["content"] + "</chat>"
|
| 32 |
+
|
| 33 |
+
def encode(self, text, **kwargs):
|
| 34 |
+
return [ord(c) + 1 for c in text]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@pytest.fixture
|
| 38 |
+
def service():
|
| 39 |
+
calls = []
|
| 40 |
+
|
| 41 |
+
def upstream(request):
|
| 42 |
+
payload = json.loads(request.content)
|
| 43 |
+
calls.append((request.url.host, payload))
|
| 44 |
+
tokens = payload["logprob_token_ids"]
|
| 45 |
+
scores = {f"token_id:{t}": -(i + 1.0) for i, t in enumerate(tokens)}
|
| 46 |
+
return httpx.Response(
|
| 47 |
+
200, json={"choices": [{"logprobs": {"top_logprobs": [scores]}}]}
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
settings = Settings(
|
| 51 |
+
"http://high:8000", "http://low:8000", "apus-high32", "apus-low16", "test-key"
|
| 52 |
+
)
|
| 53 |
+
with TestClient(
|
| 54 |
+
create_app(
|
| 55 |
+
settings,
|
| 56 |
+
Tokenizer(),
|
| 57 |
+
transport=httpx.MockTransport(upstream),
|
| 58 |
+
check_backends=False,
|
| 59 |
+
)
|
| 60 |
+
) as web:
|
| 61 |
+
yield web, calls
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def sdk_transport(web):
|
| 65 |
+
def handler(req):
|
| 66 |
+
result = web.request(
|
| 67 |
+
req.method,
|
| 68 |
+
req.url.raw_path.decode(),
|
| 69 |
+
content=req.content,
|
| 70 |
+
headers=dict(req.headers),
|
| 71 |
+
)
|
| 72 |
+
return httpx2.Response(
|
| 73 |
+
result.status_code, content=result.content, headers=dict(result.headers)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
return httpx2.MockTransport(handler)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def client(web, **kwargs):
|
| 80 |
+
return TypeSafeClient(
|
| 81 |
+
base_url="http://local",
|
| 82 |
+
api_key="test-key",
|
| 83 |
+
retry=RetryPolicy(max_retries=0),
|
| 84 |
+
transport=sdk_transport(web),
|
| 85 |
+
**kwargs,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_sync_sdk_all_primitives_and_models(service):
|
| 90 |
+
web, calls = service
|
| 91 |
+
with client(web) as sdk:
|
| 92 |
+
listed = sdk.models.list()
|
| 93 |
+
assert listed.models[0].name == "APUS-OpenJev-v1-9B"
|
| 94 |
+
assert "NOT the official" in listed.models[1].description
|
| 95 |
+
response = sdk.system_one(
|
| 96 |
+
state={"ticket": "My delivery is late."},
|
| 97 |
+
questions={
|
| 98 |
+
"queue": Choice(
|
| 99 |
+
instructions="Select the support queue.",
|
| 100 |
+
criteria={"shipping": "Delivery issues", "billing": None},
|
| 101 |
+
),
|
| 102 |
+
"refund": Noul(
|
| 103 |
+
instructions=["Decide eligibility"],
|
| 104 |
+
criteria={"true": {"rule": "eligible"}, "false": ["ineligible"]},
|
| 105 |
+
),
|
| 106 |
+
"urgency": Score(
|
| 107 |
+
instructions="Rate urgency.",
|
| 108 |
+
criteria=[
|
| 109 |
+
"Can wait",
|
| 110 |
+
{"description": "Time-sensitive"},
|
| 111 |
+
["Immediate"],
|
| 112 |
+
],
|
| 113 |
+
),
|
| 114 |
+
},
|
| 115 |
+
extra_body={"effort": "low"},
|
| 116 |
+
)
|
| 117 |
+
assert response.answers["queue"].choice == "shipping"
|
| 118 |
+
assert 0 < response.answers["refund"].noul < 1
|
| 119 |
+
assert response.answers["urgency"].legend[1] == {"description": "Time-sensitive"}
|
| 120 |
+
assert response.answers["urgency"].score == pytest.approx(
|
| 121 |
+
sum(k * v for k, v in response.answers["urgency"].probabilities.items())
|
| 122 |
+
)
|
| 123 |
+
assert response.raw_http_response.headers["x-openjev-executed-layers"] == "16"
|
| 124 |
+
assert len(calls) == 3 and all(host == "low" for host, _ in calls)
|
| 125 |
+
assert all(p["model"] == "apus-low16" and p["max_tokens"] == 1 for _, p in calls)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def test_async_sdk_default_high(service):
|
| 129 |
+
web, calls = service
|
| 130 |
+
|
| 131 |
+
async def run():
|
| 132 |
+
async with AsyncTypeSafeClient(
|
| 133 |
+
base_url="http://local",
|
| 134 |
+
api_key="test-key",
|
| 135 |
+
retry=RetryPolicy(max_retries=0),
|
| 136 |
+
transport=sdk_transport(web),
|
| 137 |
+
) as sdk:
|
| 138 |
+
assert (await sdk.models.list()).models
|
| 139 |
+
result = await sdk.system_one(
|
| 140 |
+
state="Customer needs a refund.",
|
| 141 |
+
questions={"eligible": Noul(instructions="Is this a refund request?")},
|
| 142 |
+
)
|
| 143 |
+
assert result.raw_http_response.headers["x-openjev-effort"] == "high"
|
| 144 |
+
assert result.raw_http_response.headers["x-openjev-executed-layers"] == "32"
|
| 145 |
+
|
| 146 |
+
asyncio.run(run())
|
| 147 |
+
assert calls[0][0] == "high"
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def test_auth_and_invalid_sdk_extra_body(service):
|
| 151 |
+
web, calls = service
|
| 152 |
+
assert web.get("/v1/models").status_code == 401
|
| 153 |
+
body = {"state": "ticket", "questions": {"q": {"type": "noul"}}}
|
| 154 |
+
headers = {"Authorization": "Bearer test-key"}
|
| 155 |
+
for extra in (
|
| 156 |
+
{"effort": "medium"},
|
| 157 |
+
{"unexpected": True},
|
| 158 |
+
{"model": []},
|
| 159 |
+
{"effort": {}},
|
| 160 |
+
):
|
| 161 |
+
assert web.post(
|
| 162 |
+
"/v1/systemone", json={**body, **extra}, headers=headers
|
| 163 |
+
).status_code in (404, 422)
|
| 164 |
+
assert calls == []
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def test_exact_prompt_plain_choice_and_canonical_noul():
|
| 168 |
+
criterion = {c["id"]: c["description"] for c in BINARY_CRITERIA}
|
| 169 |
+
q = prepare_request(
|
| 170 |
+
{
|
| 171 |
+
"state": "state",
|
| 172 |
+
"questions": {
|
| 173 |
+
"q": {"type": "choice", "instructions": "task", "criteria": criterion}
|
| 174 |
+
},
|
| 175 |
+
},
|
| 176 |
+
model_name="apus",
|
| 177 |
+
)[0]
|
| 178 |
+
assert q.record["criteria"] == BINARY_CRITERIA
|
| 179 |
+
nq = prepare_request(
|
| 180 |
+
{
|
| 181 |
+
"state": "state",
|
| 182 |
+
"questions": {
|
| 183 |
+
"q": {
|
| 184 |
+
"type": "noul",
|
| 185 |
+
"instructions": "task",
|
| 186 |
+
"criteria": {
|
| 187 |
+
"true": BINARY_CRITERIA[0]["description"],
|
| 188 |
+
"false": BINARY_CRITERIA[1]["description"],
|
| 189 |
+
},
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
},
|
| 193 |
+
model_name="apus",
|
| 194 |
+
)[0]
|
| 195 |
+
assert nq.record["instructions"] == "task"
|
| 196 |
+
assert nq.record["primitive"] == "noul"
|
| 197 |
+
with pytest.raises(Exception, match="budget"):
|
| 198 |
+
compile_question(nq, Tokenizer(), max_tokens=5)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def test_no_silent_low_same_engine():
|
| 202 |
+
with pytest.raises(ValueError, match="distinct engine"):
|
| 203 |
+
Settings("http://same:80", "http://same:80/", "high", "low", "key")
|
| 204 |
+
with pytest.raises(ValueError, match="aliases"):
|
| 205 |
+
Settings("http://a", "http://b", "same", "same", "key")
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def test_missing_backend_candidates_fails():
|
| 209 |
+
settings = Settings("http://a", "http://b", "high", "low", "key")
|
| 210 |
+
transport = httpx.MockTransport(
|
| 211 |
+
lambda req: httpx.Response(
|
| 212 |
+
200, json={"choices": [{"logprobs": {"top_logprobs": [{}]}}]}
|
| 213 |
+
)
|
| 214 |
+
)
|
| 215 |
+
with TestClient(
|
| 216 |
+
create_app(settings, Tokenizer(), transport=transport, check_backends=False)
|
| 217 |
+
) as web:
|
| 218 |
+
r = web.post(
|
| 219 |
+
"/v1/systemone",
|
| 220 |
+
headers={"Authorization": "Bearer key"},
|
| 221 |
+
json={"state": "x", "questions": {"q": {"type": "noul"}}},
|
| 222 |
+
)
|
| 223 |
+
assert r.status_code == 502
|
| 224 |
+
assert r.json()["error"]["code"] == "invalid_backend_scores"
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def test_duplicate_json_and_oversized_fail(service):
|
| 228 |
+
web, _ = service
|
| 229 |
+
headers = {"Authorization": "Bearer test-key"}
|
| 230 |
+
assert (
|
| 231 |
+
web.post(
|
| 232 |
+
"/v1/systemone", headers=headers, content='{"state":"a","state":"b"}'
|
| 233 |
+
).status_code
|
| 234 |
+
== 422
|
| 235 |
+
)
|
| 236 |
+
assert (
|
| 237 |
+
web.post("/v1/systemone", headers=headers, content="x" * 1_048_577).status_code
|
| 238 |
+
== 413
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_deterministic_singleton_and_unknown_model(service):
|
| 243 |
+
web, calls = service
|
| 244 |
+
headers = {"Authorization": "Bearer test-key"}
|
| 245 |
+
base = {"state": "x", "questions": {"q": {"type": "score", "criteria": ["only"]}}}
|
| 246 |
+
response = web.post("/v1/systemone", headers=headers, json=base)
|
| 247 |
+
assert response.status_code == 200
|
| 248 |
+
assert response.json()["answers"]["q"]["score"] == 0
|
| 249 |
+
assert response.headers["x-openjev-executed-layers"] == "0"
|
| 250 |
+
assert calls == []
|
| 251 |
+
base.update(model="official-other")
|
| 252 |
+
assert web.post("/v1/systemone", headers=headers, json=base).status_code == 404
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def artifact_settings(tmp_path):
|
| 256 |
+
from gateway import file_sha
|
| 257 |
+
|
| 258 |
+
high, low = tmp_path / "high", tmp_path / "low"
|
| 259 |
+
for root, depth in ((high, 32), (low, 16)):
|
| 260 |
+
root.mkdir()
|
| 261 |
+
(root / "config.json").write_text(
|
| 262 |
+
json.dumps(
|
| 263 |
+
{
|
| 264 |
+
"model_type": "qwen3_5",
|
| 265 |
+
"text_config": {
|
| 266 |
+
"num_hidden_layers": depth,
|
| 267 |
+
"hidden_size": 4096,
|
| 268 |
+
"layer_types": ["full_attention"] * depth,
|
| 269 |
+
},
|
| 270 |
+
}
|
| 271 |
+
)
|
| 272 |
+
)
|
| 273 |
+
(root / "model.safetensors.index.json").write_text("{}")
|
| 274 |
+
(high / "release-manifest.json").write_text("{}")
|
| 275 |
+
proof = {
|
| 276 |
+
"status": "verified",
|
| 277 |
+
"actual_depth": 16,
|
| 278 |
+
"all_retained_tensors_bit_identical": True,
|
| 279 |
+
}
|
| 280 |
+
for key, path in {
|
| 281 |
+
"export_config_sha256": low / "config.json",
|
| 282 |
+
"export_index_sha256": low / "model.safetensors.index.json",
|
| 283 |
+
"source_config_sha256": high / "config.json",
|
| 284 |
+
"source_index_sha256": high / "model.safetensors.index.json",
|
| 285 |
+
"source_release_manifest_sha256": high / "release-manifest.json",
|
| 286 |
+
}.items():
|
| 287 |
+
proof[key] = file_sha(path)
|
| 288 |
+
(low / "export-low-provenance.json").write_text(json.dumps(proof))
|
| 289 |
+
return Settings(
|
| 290 |
+
"http://high",
|
| 291 |
+
"http://low",
|
| 292 |
+
"high32",
|
| 293 |
+
"low16",
|
| 294 |
+
"key",
|
| 295 |
+
high_model_path=str(high),
|
| 296 |
+
low_model_path=str(low),
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def test_artifact_depth_and_provenance_fail_closed(tmp_path):
|
| 301 |
+
from gateway import verify_artifacts
|
| 302 |
+
|
| 303 |
+
settings = artifact_settings(tmp_path)
|
| 304 |
+
assert verify_artifacts(settings)["low"] == tmp_path / "low"
|
| 305 |
+
cfg = tmp_path / "low" / "config.json"
|
| 306 |
+
cfg.write_text((tmp_path / "high" / "config.json").read_text())
|
| 307 |
+
with pytest.raises(ValueError, match="depth"):
|
| 308 |
+
verify_artifacts(settings)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_startup_binds_engine_root_to_verified_depth(tmp_path):
|
| 312 |
+
settings = artifact_settings(tmp_path)
|
| 313 |
+
|
| 314 |
+
def upstream(req):
|
| 315 |
+
# Wrong low root advertises high artifacts despite a distinct low alias.
|
| 316 |
+
alias = "low16" if req.url.host == "low" else "high32"
|
| 317 |
+
return httpx.Response(
|
| 318 |
+
200, json={"data": [{"id": alias, "root": settings.high_model_path}]}
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
with pytest.raises(RuntimeError, match="alias/root"):
|
| 322 |
+
with TestClient(
|
| 323 |
+
create_app(settings, Tokenizer(), transport=httpx.MockTransport(upstream))
|
| 324 |
+
):
|
| 325 |
+
pass
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
def test_sdk_rejects_unknown_extension_via_http(service):
|
| 329 |
+
from typesafe_sdk import TypeSafeUnprocessableEntityError
|
| 330 |
+
|
| 331 |
+
web, _ = service
|
| 332 |
+
with client(web) as sdk, pytest.raises(TypeSafeUnprocessableEntityError):
|
| 333 |
+
sdk.system_one(
|
| 334 |
+
state="Customer ticket", questions={"q": Noul()}, extra_body={"unknown": 1}
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def test_live_smoke_script_with_real_sdk(service, tmp_path, monkeypatch):
|
| 339 |
+
import smoke_sdk
|
| 340 |
+
|
| 341 |
+
web, _ = service
|
| 342 |
+
monkeypatch.setenv("JEV_API_KEY", "test-key")
|
| 343 |
+
monkeypatch.setattr(
|
| 344 |
+
smoke_sdk,
|
| 345 |
+
"TypeSafeClient",
|
| 346 |
+
lambda **kw: TypeSafeClient(transport=sdk_transport(web), **kw),
|
| 347 |
+
)
|
| 348 |
+
monkeypatch.setattr(
|
| 349 |
+
smoke_sdk,
|
| 350 |
+
"AsyncTypeSafeClient",
|
| 351 |
+
lambda **kw: AsyncTypeSafeClient(transport=sdk_transport(web), **kw),
|
| 352 |
+
)
|
| 353 |
+
output = tmp_path / "smoke"
|
| 354 |
+
monkeypatch.setattr(sys, "argv", ["smoke_sdk.py", "--output", str(output)])
|
| 355 |
+
smoke_sdk.main()
|
| 356 |
+
report = json.loads((output / "summary.json").read_text())
|
| 357 |
+
assert report["status"] == "passed" and report["cases"] == 5
|
| 358 |
+
rows = [
|
| 359 |
+
json.loads(line)
|
| 360 |
+
for line in (output / "requests.jsonl").read_text().splitlines()
|
| 361 |
+
]
|
| 362 |
+
assert rows[0]["response_headers"]["x-typesafe-request-id"]
|
| 363 |
+
assert rows[1]["request"]["questions"]["queue"]["type"] == "choice"
|
| 364 |
+
assert "test-key" not in (output / "requests.jsonl").read_text()
|
deployment/tests/test_vllm_depth_export.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CPU proof: exported half-depth uses original layers, norm, and readout."""
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
import torch
|
| 9 |
+
from safetensors import safe_open
|
| 10 |
+
from safetensors.torch import save_file
|
| 11 |
+
from transformers import (
|
| 12 |
+
Qwen3_5Config,
|
| 13 |
+
Qwen3_5ForConditionalGeneration,
|
| 14 |
+
Qwen3_5TextConfig,
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
spec = importlib.util.spec_from_file_location(
|
| 19 |
+
"export_low", Path(__file__).parents[1] / "export_low.py"
|
| 20 |
+
)
|
| 21 |
+
module = importlib.util.module_from_spec(spec)
|
| 22 |
+
spec.loader.exec_module(module)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_actual_32_to_16_prefix_logits_and_saved_tensors(tmp_path):
|
| 26 |
+
torch.set_num_threads(1)
|
| 27 |
+
text = Qwen3_5TextConfig(
|
| 28 |
+
vocab_size=64,
|
| 29 |
+
hidden_size=32,
|
| 30 |
+
intermediate_size=48,
|
| 31 |
+
num_hidden_layers=32,
|
| 32 |
+
num_attention_heads=2,
|
| 33 |
+
num_key_value_heads=1,
|
| 34 |
+
head_dim=16,
|
| 35 |
+
linear_key_head_dim=8,
|
| 36 |
+
linear_value_head_dim=8,
|
| 37 |
+
linear_num_key_heads=2,
|
| 38 |
+
linear_num_value_heads=2,
|
| 39 |
+
layer_types=(["linear_attention"] * 3 + ["full_attention"]) * 8,
|
| 40 |
+
rope_parameters={
|
| 41 |
+
"rope_type": "default",
|
| 42 |
+
"rope_theta": 10000.0,
|
| 43 |
+
"partial_rotary_factor": 1.0,
|
| 44 |
+
"mrope_section": [2, 3, 3],
|
| 45 |
+
},
|
| 46 |
+
)
|
| 47 |
+
config = Qwen3_5Config(
|
| 48 |
+
text_config=text,
|
| 49 |
+
vision_config={
|
| 50 |
+
"depth": 1,
|
| 51 |
+
"hidden_size": 32,
|
| 52 |
+
"intermediate_size": 48,
|
| 53 |
+
"num_heads": 2,
|
| 54 |
+
"out_hidden_size": 32,
|
| 55 |
+
"num_position_embeddings": 16,
|
| 56 |
+
},
|
| 57 |
+
)
|
| 58 |
+
config._attn_implementation = "eager"
|
| 59 |
+
config.text_config._attn_implementation = "eager"
|
| 60 |
+
config.text_config.num_hidden_layers = 32
|
| 61 |
+
config.text_config.layer_types = (["linear_attention"] * 3 + ["full_attention"]) * 8
|
| 62 |
+
config.tie_word_embeddings = False
|
| 63 |
+
config.text_config.tie_word_embeddings = False
|
| 64 |
+
torch.manual_seed(47)
|
| 65 |
+
full = Qwen3_5ForConditionalGeneration(config).eval()
|
| 66 |
+
state = full.state_dict()
|
| 67 |
+
source, output = tmp_path / "source", tmp_path / "low"
|
| 68 |
+
source.mkdir()
|
| 69 |
+
output.mkdir()
|
| 70 |
+
save_file(state, source / "model.safetensors", metadata={"format": "pt"})
|
| 71 |
+
weight_map = {k: "model.safetensors" for k in state}
|
| 72 |
+
kept, removed = module.select_keys(weight_map, 16, 32)
|
| 73 |
+
assert len(removed) > 0
|
| 74 |
+
assert module.READOUT <= set(kept)
|
| 75 |
+
assert all(
|
| 76 |
+
not module.LAYER.match(k) or int(module.LAYER.match(k).group(1)) < 16
|
| 77 |
+
for k in kept
|
| 78 |
+
)
|
| 79 |
+
files, tensors = module.copy_weights(source, output, weight_map, kept)
|
| 80 |
+
short_config = module.prefix_config(config.to_dict())
|
| 81 |
+
assert short_config["vision_config"] == config.to_dict()["vision_config"]
|
| 82 |
+
assert short_config["text_config"]["num_hidden_layers"] == 16
|
| 83 |
+
assert len(short_config["text_config"]["layer_types"]) == 16
|
| 84 |
+
loaded = {}
|
| 85 |
+
for f in files:
|
| 86 |
+
with safe_open(output / f["file"], framework="pt") as handle:
|
| 87 |
+
loaded.update({key: handle.get_tensor(key) for key in handle.keys()})
|
| 88 |
+
low_config = Qwen3_5Config.from_dict(short_config)
|
| 89 |
+
low_config._attn_implementation = "eager"
|
| 90 |
+
low_config.text_config._attn_implementation = "eager"
|
| 91 |
+
low = Qwen3_5ForConditionalGeneration(low_config).eval()
|
| 92 |
+
low.load_state_dict(loaded, strict=True)
|
| 93 |
+
assert len(low.model.language_model.layers) == 16
|
| 94 |
+
assert len(tensors) == len(loaded)
|
| 95 |
+
for key in module.READOUT:
|
| 96 |
+
torch.testing.assert_close(loaded[key], state[key], rtol=0, atol=0)
|
| 97 |
+
ids, candidates = torch.tensor([[4, 5, 6, 7]]), torch.tensor([10, 11, 12])
|
| 98 |
+
with torch.no_grad():
|
| 99 |
+
captured = []
|
| 100 |
+
hook = full.model.language_model.layers[15].register_forward_hook(
|
| 101 |
+
lambda _module, _inputs, output: captured.append(output.detach().clone())
|
| 102 |
+
)
|
| 103 |
+
try:
|
| 104 |
+
full(input_ids=ids, use_cache=False)
|
| 105 |
+
finally:
|
| 106 |
+
hook.remove()
|
| 107 |
+
# Independently capture the full model's layer-16 residual, then apply
|
| 108 |
+
# its original final norm and head. No monorepo runtime dependency.
|
| 109 |
+
native = full.lm_head(
|
| 110 |
+
full.model.language_model.norm(captured[0][:, -1])
|
| 111 |
+
).index_select(-1, candidates)
|
| 112 |
+
exported = (
|
| 113 |
+
low(input_ids=ids, use_cache=False)
|
| 114 |
+
.logits[:, -1]
|
| 115 |
+
.index_select(-1, candidates)
|
| 116 |
+
)
|
| 117 |
+
torch.testing.assert_close(native, exported, atol=1e-6, rtol=1e-5)
|
| 118 |
+
assert (
|
| 119 |
+
json.loads((output / "model.safetensors.index.json").read_text())["metadata"][
|
| 120 |
+
"total_size"
|
| 121 |
+
]
|
| 122 |
+
> 0
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@pytest.mark.parametrize("missing", sorted(module.READOUT))
|
| 127 |
+
def test_readout_cannot_be_dropped(missing):
|
| 128 |
+
keys = (module.READOUT - {missing}) | {
|
| 129 |
+
f"model.language_model.layers.{i}.weight" for i in range(32)
|
| 130 |
+
}
|
| 131 |
+
with pytest.raises(ValueError, match="missing"):
|
| 132 |
+
module.select_keys(keys, 16, 32)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def test_unknown_adapter_namespace_rejected():
|
| 136 |
+
keys = (
|
| 137 |
+
module.READOUT
|
| 138 |
+
| {f"model.language_model.layers.{i}.weight" for i in range(32)}
|
| 139 |
+
| {"readout.adapter.weight"}
|
| 140 |
+
)
|
| 141 |
+
with pytest.raises(ValueError, match="unexpected"):
|
| 142 |
+
module.select_keys(keys, 16, 32)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_existing_output_never_overwritten(tmp_path):
|
| 146 |
+
with pytest.raises(ValueError, match="new directory"):
|
| 147 |
+
module.export(tmp_path / "unused", tmp_path)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def test_source_stat_change_rejects_verified_publication(tmp_path):
|
| 151 |
+
path = tmp_path / "weights"
|
| 152 |
+
path.write_bytes(b"original")
|
| 153 |
+
snapshot = module.source_snapshot(tmp_path)
|
| 154 |
+
module.assert_source_unchanged(tmp_path, snapshot)
|
| 155 |
+
path.write_bytes(b"changed-longer")
|
| 156 |
+
with pytest.raises(ValueError, match="changed during export"):
|
| 157 |
+
module.assert_source_unchanged(tmp_path, snapshot)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def test_launcher_full_low_weight_hash_detects_tampering(tmp_path):
|
| 161 |
+
config = {
|
| 162 |
+
"model_type": "qwen3_5",
|
| 163 |
+
"text_config": {
|
| 164 |
+
"hidden_size": 4096,
|
| 165 |
+
"vocab_size": 248320,
|
| 166 |
+
"num_hidden_layers": 16,
|
| 167 |
+
"layer_types": ["linear_attention"] * 16,
|
| 168 |
+
},
|
| 169 |
+
}
|
| 170 |
+
(tmp_path / "config.json").write_text(json.dumps(config))
|
| 171 |
+
keys = module.READOUT | {
|
| 172 |
+
f"model.language_model.layers.{i}.weight" for i in range(16)
|
| 173 |
+
}
|
| 174 |
+
(tmp_path / "model.safetensors.index.json").write_text(
|
| 175 |
+
json.dumps({"weight_map": {key: "model.safetensors" for key in keys}})
|
| 176 |
+
)
|
| 177 |
+
shard = tmp_path / "model.safetensors"
|
| 178 |
+
shard.write_bytes(b"original-weights")
|
| 179 |
+
manifest = {
|
| 180 |
+
"status": "verified",
|
| 181 |
+
"actual_depth": 16,
|
| 182 |
+
"all_retained_tensors_bit_identical": True,
|
| 183 |
+
"export_config_sha256": module.file_sha(tmp_path / "config.json"),
|
| 184 |
+
"export_index_sha256": module.file_sha(
|
| 185 |
+
tmp_path / "model.safetensors.index.json"
|
| 186 |
+
),
|
| 187 |
+
"export_weight_files": [
|
| 188 |
+
{
|
| 189 |
+
"file": shard.name,
|
| 190 |
+
"bytes": shard.stat().st_size,
|
| 191 |
+
"sha256": module.file_sha(shard),
|
| 192 |
+
}
|
| 193 |
+
],
|
| 194 |
+
}
|
| 195 |
+
(tmp_path / "export-low-provenance.json").write_text(json.dumps(manifest))
|
| 196 |
+
module.verify_artifact(tmp_path, 16)
|
| 197 |
+
shard.write_bytes(b"tampered-weights")
|
| 198 |
+
with pytest.raises(ValueError, match="weight SHA mismatch"):
|
| 199 |
+
module.verify_artifact(tmp_path, 16)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def test_launcher_high_release_weight_hash_detects_tampering(tmp_path):
|
| 203 |
+
config = {
|
| 204 |
+
"model_type": "qwen3_5",
|
| 205 |
+
"architectures": ["Qwen3_5ForConditionalGeneration"],
|
| 206 |
+
"tie_word_embeddings": False,
|
| 207 |
+
"text_config": {
|
| 208 |
+
"hidden_size": 4096,
|
| 209 |
+
"vocab_size": 248320,
|
| 210 |
+
"intermediate_size": 12288,
|
| 211 |
+
"num_hidden_layers": 32,
|
| 212 |
+
"tie_word_embeddings": False,
|
| 213 |
+
"layer_types": (["linear_attention"] * 3 + ["full_attention"]) * 8,
|
| 214 |
+
},
|
| 215 |
+
}
|
| 216 |
+
(tmp_path / "config.json").write_text(json.dumps(config))
|
| 217 |
+
(tmp_path / "depth_config.json").write_text(
|
| 218 |
+
json.dumps({"exit_depth": 16, "full_depth": 32})
|
| 219 |
+
)
|
| 220 |
+
for name in ("tokenizer.json", "tokenizer_config.json", "chat_template.jinja"):
|
| 221 |
+
(tmp_path / name).write_text("mock tokenization asset")
|
| 222 |
+
keys = module.READOUT | {
|
| 223 |
+
f"model.language_model.layers.{i}.weight" for i in range(32)
|
| 224 |
+
}
|
| 225 |
+
(tmp_path / "model.safetensors.index.json").write_text(
|
| 226 |
+
json.dumps({"weight_map": {key: "model.safetensors" for key in keys}})
|
| 227 |
+
)
|
| 228 |
+
shard = tmp_path / "model.safetensors"
|
| 229 |
+
shard.write_bytes(b"original-weights")
|
| 230 |
+
entries = [
|
| 231 |
+
{"path": p.name, "sha256": module.file_sha(p)} for p in tmp_path.iterdir()
|
| 232 |
+
]
|
| 233 |
+
(tmp_path / "release-manifest.json").write_text(json.dumps({"files": entries}))
|
| 234 |
+
module.verify_artifact(tmp_path, 32)
|
| 235 |
+
shard.write_bytes(b"tampered-weights")
|
| 236 |
+
with pytest.raises(ValueError, match="weight SHA mismatch"):
|
| 237 |
+
module.verify_artifact(tmp_path, 32)
|