kelseyway commited on
Commit
5c0a4a8
·
1 Parent(s): 105f9ef

Make MLX adapter-only and include the reproducible BF16 converter

Browse files

The MLX setup reads the root adapter and heads, downloads the pinned Qwen base
separately, and assembles a verified local BF16 model. Includes the exact historical
converter, locked dependencies, source, integrity tests, setup instructions and dated
CUDA-parity status. No new temperature fitting; complete held-out parity remains
pending.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. mlx/.gitignore +12 -0
  2. mlx/CONVERTER-SOURCE.json +21 -0
  3. mlx/LICENSE +202 -0
  4. mlx/MODIFICATIONS.md +25 -0
  5. mlx/NOTICE +78 -0
  6. mlx/README.md +146 -0
  7. mlx/bf16/LICENSE +202 -0
  8. mlx/bf16/MODIFICATIONS.md +75 -0
  9. mlx/bf16/NOTICE +78 -0
  10. mlx/bf16/README.md +51 -0
  11. mlx/bf16/base-manifest.json +197 -0
  12. mlx/bf16/binding.json +141 -0
  13. mlx/bf16/conversion.json +203 -0
  14. mlx/docs/UPSTREAM-MODIFICATIONS.md +75 -0
  15. mlx/docs/VALIDATION-20260921.md +46 -0
  16. mlx/examples/decide.py +21 -0
  17. mlx/pyproject.toml +35 -0
  18. mlx/requirements.cloud.lock +1072 -0
  19. mlx/requirements.lock +79 -0
  20. mlx/scripts/benchmark.py +122 -0
  21. mlx/scripts/benchmark_chunks.py +50 -0
  22. mlx/scripts/check_cuda_parity.py +172 -0
  23. mlx/scripts/score_parity.py +19 -0
  24. mlx/scripts/validate_api.py +128 -0
  25. mlx/src/solomon_mlx/__init__.py +5 -0
  26. mlx/src/solomon_mlx/_vendor/__init__.py +2 -0
  27. mlx/src/solomon_mlx/_vendor/contract.py +111 -0
  28. mlx/src/solomon_mlx/_vendor/evidence.py +156 -0
  29. mlx/src/solomon_mlx/_vendor/evidence_v3.py +425 -0
  30. mlx/src/solomon_mlx/_vendor/prompts.py +43 -0
  31. mlx/src/solomon_mlx/_vendor/retrieval.py +152 -0
  32. mlx/src/solomon_mlx/_vendor/semantics.py +71 -0
  33. mlx/src/solomon_mlx/api.py +321 -0
  34. mlx/src/solomon_mlx/artifacts.py +77 -0
  35. mlx/src/solomon_mlx/budget.py +71 -0
  36. mlx/src/solomon_mlx/cli.py +76 -0
  37. mlx/src/solomon_mlx/engine.py +333 -0
  38. mlx/src/solomon_mlx/evaluation.py +324 -0
  39. mlx/src/solomon_mlx/prepare.py +121 -0
  40. mlx/src/solomon_mlx_hub/__init__.py +5 -0
  41. mlx/src/solomon_mlx_hub/__main__.py +50 -0
  42. mlx/src/solomon_mlx_hub/data/base-manifest.json +197 -0
  43. mlx/src/solomon_mlx_hub/data/release.json +105 -0
  44. mlx/src/solomon_mlx_hub/prepare.py +212 -0
  45. mlx/tests/test_artifacts_and_evaluation.py +77 -0
  46. mlx/tests/test_budget.py +24 -0
  47. mlx/tests/test_contract.py +96 -0
  48. mlx/tests/test_hub.py +173 -0
  49. mlx/tests/test_metal_primitives.py +40 -0
  50. mlx/tests/test_parity.py +24 -0
mlx/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ *.egg-info/
6
+ snapshots/
7
+ models/
8
+ evaluations/
9
+ *.log
10
+
11
+ document-replay.json
12
+ dist/
mlx/CONVERTER-SOURCE.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "converter_code_sha256": "bbcae17fc1c35db80a79d5865133a42ef9a1b0cf342fff71949972e69cce43ec",
3
+ "files": {
4
+ "__init__.py": "714a5439b33780d250c404938cd062c0d6809f83f21b59f89997bd168469925a",
5
+ "_vendor/__init__.py": "77defd15cc47661e0e9a31275e7fd59e979f3f9a6429671d8199f6002dca6c2c",
6
+ "_vendor/contract.py": "7c6607179a028f30b462349a86f9cf7d0ec2652e0a1c7c8c24649b6ce71df697",
7
+ "_vendor/evidence.py": "18a4978a27d6bbd3f7daaa34836d1820303c4ff581b0dfda877aab586392293b",
8
+ "_vendor/evidence_v3.py": "6ff3da1e920c082e9b3629ca0230007262f2ce69fd921b89dd4b3e67511b3a64",
9
+ "_vendor/prompts.py": "8bfb5a12d625664c8830f5cf243aac04d1e973bfe4f5ecfc8221021f09c318df",
10
+ "_vendor/retrieval.py": "11e7bbb1a84ba837eac3768d6e882f44a557be25d62729c8a39dd5cb870c0932",
11
+ "_vendor/semantics.py": "b580d3c114536a6faf58bcd92d2c61028f80891a19d78d6a17ed585948abfc15",
12
+ "api.py": "bae9fdd3ef6b395142ec5c099635cc9ba90f7f33c7423f99caf6009f51d4fd3c",
13
+ "artifacts.py": "d41d4d3f19aaf0dd54e4503d43807a29208d9528b8d4734b9f3d99cad18d9f93",
14
+ "budget.py": "f72614686c19e1d923e6bc23d4353e8b1ac150863565939ae4a3831bc4ecd211",
15
+ "cli.py": "0a6be4cd03621963ec5f73a8af71224cb8b40ae88232477d8370b7d3b9b75021",
16
+ "engine.py": "13c27c3fbca576ae0d70262c2a75c9382df3b4fcfd8a11996d125750883b648a",
17
+ "evaluation.py": "bb9f50d184527e21139b9ffb5e9d1613bc30822ccd7cf06b7b7f8b1cc8b50674",
18
+ "prepare.py": "dda9879a9ddc1c3f6dcf0df52b3a244b2ca7920946bbd29ddc8e1c0f9944b887"
19
+ },
20
+ "schema": "solomon-mlx-converter-source-v1"
21
+ }
mlx/LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Doccy Pty Ltd
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
mlx/MODIFICATIONS.md ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Solomon MLX modifications
2
+
3
+ This port adapts Doccy Pty Ltd’s Apache-2.0 Solomon source at revision
4
+ `2ec506902269e4636285c8811f6c0f52c9300c0c`. Original modification notices are preserved in
5
+ `docs/UPSTREAM-MODIFICATIONS.md`.
6
+
7
+ Changes:
8
+
9
+ - Replaced CUDA execution with MLX-VLM Qwen3.5 execution for the declared Qwen3.8 architecture.
10
+ - Added BF16 shard conversion with FP32 normalization parameters, adapter, trained heads and recurrent states.
11
+ - Replaced global adapter state with instance-owned state and isolated question cache containers.
12
+ - Removed vocabulary projection from decision inference.
13
+ - Added a document-state API, replay recipes, artifact checksums and MLX-specific runtime identities.
14
+ - Extracted the original prompts, question contract, semantics and evidence utilities into `_vendor`.
15
+ - Implemented the documented ordering-score product locally because the release omits `scope9.reliability`.
16
+ - Added download, CUDA parity comparison, benchmark and test tools. New temperature fitting is outside the current scope.
17
+
18
+ No claim is made that this port inherits CUDA calibration or qualification. See
19
+ `docs/VALIDATION-20260921.md` for measured results and outstanding validation.
20
+
21
+ Adapter-only distribution update (21 September 2026): added a separate verified Hub
22
+ loader, atomic local assembly and cache reuse; retained the exact historical converter
23
+ source and FP32 normalization handling. Base weights are downloaded from the pinned
24
+ Qwen repository. Duplicate adapter/head files under `mlx/bf16` are replaced by the
25
+ root `adapter/` copies. CUDA parity status is reported separately from conversion.
mlx/NOTICE ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Solomon v1.1.0
2
+ Copyright 2026 Doccy Pty Ltd
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
5
+ this work except in compliance with the License. You may obtain a copy of the
6
+ License in the LICENSE file distributed with this work, or at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ --------------------------------------------------------------------------------
11
+ ATTRIBUTION FOR THE BASE MODEL (Apache License 2.0, section 4(c))
12
+ --------------------------------------------------------------------------------
13
+
14
+ This work is a DERIVATIVE WORK of:
15
+
16
+ Qwen/Qwen3.8-27B
17
+ Copyright 2026 Alibaba Cloud
18
+ Licensed under the Apache License, Version 2.0
19
+ Pinned revision: 1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0
20
+ Licence text: licenses/Qwen-Apache-2.0.txt (sha256 bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a, 11544 bytes,
21
+ the exact bytes served at the pinned revision)
22
+
23
+ The base model weights are NOT redistributed in this repository. They are
24
+ referenced by the pinned revision above and downloaded by the operator directly
25
+ from the upstream repository under the upstream licence.
26
+
27
+ Upstream NOTICE file: NONE. A NOTICE file is absent from Qwen/Qwen3.8-27B at the
28
+ pinned revision (HTTP 404, retrieved 2026-09-18T03:49Z without credentials).
29
+ Apache-2.0 section 4(d) therefore imposes no carry-forward obligation here. No
30
+ upstream attribution text has been invented or paraphrased.
31
+
32
+ "Qwen" and "Alibaba Cloud" are used nominatively, to identify the base model
33
+ this adapter was trained against. Apache-2.0 section 6 grants no trademark
34
+ rights and none are claimed or implied. Nothing here states or implies any
35
+ endorsement, sponsorship or affiliation.
36
+
37
+ --------------------------------------------------------------------------------
38
+ STATEMENT OF CHANGES (Apache License 2.0, section 4(b))
39
+ --------------------------------------------------------------------------------
40
+
41
+ No upstream source file is modified, and no upstream file is redistributed
42
+ except the unmodified licence text at licenses/Qwen-Apache-2.0.txt.
43
+
44
+ The modification this work carries is a trained LoRA adapter and a set of
45
+ trained linear answer heads, applied to the base model at inference time:
46
+
47
+ * LoRA adapter, rank 64, question-side placement, float32
48
+ adapter/adapter.safetensors
49
+ sha256 2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca
50
+ 870363376 bytes
51
+ * Trained linear answer heads
52
+ adapter/heads.npz
53
+ sha256 126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f
54
+ 2053938 bytes
55
+ * Readout calibration (one positive scalar per task)
56
+ serving/scope9-readout-temperature-v2.json
57
+ file sha256 baa7263ca9e865dda230617563e6a615de5050adc620c28924dffef68d527118
58
+ payload sha256 682d611ec53c3322905c7e265bf9b9b35a1cd581f6d8b7ea924df864a3d74278
59
+
60
+ Training took place between 16 and 20 September 2026. The adapter and the heads
61
+ are identified by the checksums above. Full change detail is in MODIFICATIONS.md.
62
+
63
+ --------------------------------------------------------------------------------
64
+ THIRD-PARTY CONTENT IN THIS REPOSITORY
65
+ --------------------------------------------------------------------------------
66
+
67
+ The only third-party content distributed here is the unmodified Apache License
68
+ 2.0 text at licenses/Qwen-Apache-2.0.txt, reproduced to satisfy section 4(a).
69
+
70
+ No training document, evaluation panel, dataset, corpus, rendered page, cached
71
+ state, score archive or acceptance fixture is distributed. Every file in this
72
+ repository was scanned for text originating in third-party source documents
73
+ before release; see MODIFICATIONS.md, "Third-party text scan".
74
+
75
+ Everything else in this repository -- the serving code under src/, the adapter
76
+ and head weights, the calibration artifact, the serving binding and the
77
+ documentation -- is original work of Doccy Pty Ltd, licensed under
78
+ Apache-2.0.
mlx/README.md ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Solomon MLX
2
+
3
+ Full BF16 Solomon v1.1 inference on Apple Silicon. The repository distributes
4
+ adapters and source code; the Qwen backbone is downloaded separately from its
5
+ pinned upstream revision and converted locally. No quantization or LoRA merging
6
+ is performed. Full-model measurements use an M5 Max with 128 GB memory, with
7
+ about 59.9 GB peak Metal allocation on the focused fixtures. Longer documents
8
+ and more images require additional memory; 24–32 GB Macs cannot run this profile.
9
+
10
+ **Status: experimental.** Focused text and image decisions match CUDA. Complete
11
+ held-out parity remains pending. Qualification is CUDA parity only: no new
12
+ calibration or temperature fitting. See [validation results](docs/VALIDATION-20260921.md).
13
+
14
+ ## Install from this repository
15
+
16
+ Use Python 3.12 or 3.13 on Apple Silicon. Pin the full repository commit shown on
17
+ Hugging Face, including after any repository history rewrite. The historical
18
+ Solomon source revision remains provenance; it is not required to be downloadable.
19
+
20
+ ```sh
21
+ # From a source checkout, enter its mlx/ directory first.
22
+ uv sync --frozen --extra dev
23
+
24
+ # Obtain the current commit once, then retain it for repeatable downloads.
25
+ SOLOMON_COMMIT=$(uv run python -c 'from huggingface_hub import HfApi; print(HfApi().model_info("DoccyHealth/Solomon").sha)')
26
+ uv run solomon-mlx-hub prepare --revision "$SOLOMON_COMMIT" --output models/quality
27
+ ```
28
+
29
+ Authenticate with `hf auth login` first if the repository requires access. This
30
+ package is supplied here as source; it is not claimed to be published on PyPI.
31
+ For a checkout without downloading model files through Git LFS:
32
+
33
+ ```sh
34
+ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/DoccyHealth/Solomon
35
+ cd Solomon/mlx
36
+ ```
37
+
38
+ The setup tool explicitly fetches `adapter/**` and the retained MLX licensing
39
+ metadata. It downloads `Qwen/Qwen3.8-27B` at
40
+ `1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0`, verifies every input against the
41
+ bundled manifest, and converts one shard at a time. Budget about 112 GB of disk
42
+ for original and converted weights, plus temporary space and caches. Existing
43
+ original base files can be reused with `--base /path/to/original-qwen`.
44
+
45
+ The output contains `backbone/`, the unmerged adapter, trained heads, notices
46
+ and a fresh `binding.json`. Existing valid outputs are verified and reused;
47
+ corrupt or incompatible outputs fail without being overwritten. Conversion is
48
+ atomic and concurrent preparations into the same output are rejected. Interrupted
49
+ conversions may leave a hidden temporary directory; the final output is never
50
+ marked ready before verification completes.
51
+
52
+ For fully offline preparation, provide both downloaded inputs:
53
+
54
+ ```sh
55
+ uv run solomon-mlx-hub prepare --revision "$SOLOMON_COMMIT" \
56
+ --snapshot /path/to/solomon-snapshot --base /path/to/original-qwen \
57
+ --output models/quality
58
+ uv run solomon-mlx-hub verify models/quality
59
+ ```
60
+
61
+ CPU conversion is the default. `--device gpu` selects Metal conversion on a Mac.
62
+ Linux CPU conversion is also supported by the existing converter and can use
63
+ `uv sync --frozen --extra cloud` with MLX's CPU backend. Linux conversion does
64
+ not run Apple Metal inference or establish CUDA parity.
65
+
66
+ ## Converter and provenance
67
+
68
+ The exact converter is [src/solomon_mlx/prepare.py](src/solomon_mlx/prepare.py).
69
+ The runtime Python sources are unchanged in behaviour. The only edits made for
70
+ this release rename the runtime identity's `source_contract` field to `solomon-v1`
71
+ and adjust comments, docstrings and one error message, so their combined
72
+ `converter_code_sha256` is
73
+ `bbcae17fc1c35db80a79d5865133a42ef9a1b0cf342fff71949972e69cce43ec`.
74
+ It is computed by `solomon_mlx.artifacts.code_identity()` from the sorted mapping
75
+ of relative Python paths to SHA-256 values. The new download/assembly wrapper is
76
+ in the separate `solomon_mlx_hub` package.
77
+
78
+ The converter retains large weights in BF16 and promotes normalization weights,
79
+ `A_log` and `dt_bias` to FP32 **before** applying upstream normalization offsets.
80
+ It validates tensor names and shapes with MLX-VLM's Qwen3.5 implementation. The
81
+ original FP32 adapter and ten trained heads are copied without changes. The
82
+ runtime applies the adapter only to question tokens, with its explicit 2.0 scale.
83
+
84
+ `bf16/conversion.json` and `bf16/binding.json` describe the historical cloud
85
+ conversion; paths inside them describe the original local model layout. They
86
+ are provenance, not a manifest of files currently present on the Hub, and they
87
+ record the converter hash of that historical conversion, `648e440cface0838f2dcc8d89b3ab172d97f4ffc1f88fe0b7cbbe3ca73b8a575`,
88
+ which predates the documentation edits described above. The root
89
+ adapter files have the same hashes as their removed duplicates. Newly converted
90
+ safetensors may serialize differently across CPU and Metal, so new outputs get
91
+ actual output checksums and their own runtime binding. Historical CUDA or MLX
92
+ qualification identities are never reused for a different artifact binding.
93
+
94
+ ## Python API
95
+
96
+ ```python
97
+ from solomon_mlx import Solomon
98
+
99
+ model = Solomon.load("models/quality", profile="quality")
100
+ with model.prefill("Rookwood Ltd holds a current certification.") as state:
101
+ result = model.decide(
102
+ state=state,
103
+ questions={"certified": {
104
+ "type": "noul",
105
+ "instructions": "Does Rookwood Ltd hold a current certification?",
106
+ }},
107
+ evidence="support",
108
+ )
109
+ print(result["answers"]["certified"])
110
+ ```
111
+
112
+ Or use `solomon_mlx_hub.load("models/quality", revision=COMMIT)` to prepare and
113
+ load in one call. `revision` must be a full 40-character commit SHA. Cached valid
114
+ outputs are reused without network access and retain their original binding.
115
+
116
+ Documents accept text, structured JSON objects, or ordered `{"text": ...}` and
117
+ `{"image": local_path}` parts. Image features are computed once per document.
118
+ There is no PDF renderer or OCR. Question forms preserve Boolean, entity and
119
+ multilabel `noul`, single `choice`, and ordered `score` semantics; candidate order
120
+ is retained. Missing/conflicting facts collapse before temperature application.
121
+ The API defaults to T=1. No new temperatures are fitted by setup or parity checks.
122
+
123
+ Evidence levels are `none`, `support`, `sufficiency`, and `removal`. Text spans use
124
+ exact code-point offsets. Sufficiency and removal re-encode the relevant source;
125
+ they do not establish causal faithfulness. Image evidence requires `page_selector=`;
126
+ otherwise the result reports `unsupported_page_selector`. States belong to one
127
+ model instance and support `close()`, `save(path)`, and `model.replay(path)`.
128
+ Replay persists a checksummed source recipe and recomputes caches.
129
+
130
+ ## Tests
131
+
132
+ ```sh
133
+ uv run pytest -q
134
+ uv run ruff check src tests scripts
135
+ ```
136
+
137
+ Tests use synthetic small models to cover conversion precision, CPU/Metal tensor
138
+ agreement, cache isolation, text/image processing, answer semantics, evidence,
139
+ artifact corruption, and adapter-only setup. These tests do not replace trained
140
+ model parity measurements. The included parity checker compares saved CUDA and
141
+ MLX scores using the same frozen temperatures exactly once, reports probability
142
+ drift separately, and cannot pass a complete-panel gate from partial results.
143
+ Evaluation inputs and private infrastructure configuration are not distributed.
144
+
145
+ Preserve `LICENSE`, `NOTICE`, `MODIFICATIONS.md`, and upstream model notices with
146
+ permitted copies. Model downloads and conversion do not alter repository visibility.
mlx/bf16/LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Doccy Pty Ltd
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
mlx/bf16/MODIFICATIONS.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Statement of changes
2
+
3
+ Apache License 2.0, section 4(b): prominent notice that files carry modifications.
4
+
5
+ ## What is modified
6
+
7
+ **No upstream source file is modified.** No file from `Qwen/Qwen3.8-27B` is edited,
8
+ renamed, patched or redistributed in this repository, with one exception: the
9
+ upstream licence text is reproduced byte-for-byte at `licenses/Qwen-Apache-2.0.txt`
10
+ (sha256 `bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a`) because section 4(a) requires it. That file is
11
+ unmodified and carries the upstream copyright.
12
+
13
+ The modification this work carries is not an edit to a source file. It is a set
14
+ of **trained parameters applied to the base model at inference time**, plus an
15
+ original serving layer that reads the model's logits. Concretely:
16
+
17
+ | Change | Artifact | sha256 |
18
+ |---|---|---|
19
+ | LoRA adapter, rank 64, question-side placement, float32 | `adapter/adapter.safetensors` | `2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca` |
20
+ | Trained linear answer heads | `adapter/heads.npz` | `126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f` |
21
+ | Readout calibration, one positive scalar per task | `serving/scope9-readout-temperature-v2.json` | file `baa7263ca9e865dda230617563e6a615de5050adc620c28924dffef68d527118` / payload `682d611ec53c3322905c7e265bf9b9b35a1cd581f6d8b7ea924df864a3d74278` |
22
+ | Runtime identity binding | `serving/serving-binding.json` | payload `95683c1f87ec3f71b7657669dc311918ff53b7d41a981eaa8d827047e72cb505` |
23
+ | Serving layer (original work, not derived from upstream code) | `src/` | see `MANIFEST.json` |
24
+
25
+ Base model, unmodified and not redistributed: `Qwen/Qwen3.8-27B` at revision
26
+ `1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0`, Copyright 2026 Alibaba Cloud, Apache-2.0.
27
+
28
+ Training took place between 16 and 20 September 2026. The shipped artifacts are
29
+ identified by the checksums in the table above and in `MANIFEST.json`.
30
+
31
+ ## Which files carry a change notice
32
+
33
+ | File | Why |
34
+ |---|---|
35
+ | `NOTICE` | Section 4(b) and 4(c) statement, carried with every copy |
36
+ | `MODIFICATIONS.md` | this file |
37
+ | `README.md` | names the base model, the pinned revision and the derivative relationship in the front matter and in the body |
38
+ | `adapter/config.json` | machine-readable record of the base repo, the pinned revision and both weight checksums |
39
+ | `serving/serving-binding.json` | pins the exact runtime the weights were measured on and refuses to load against a different one |
40
+
41
+ No file under `src/` carries an upstream change notice, because no file under
42
+ `src/` contains upstream code. Every file there is original work, written for
43
+ this project, and is covered by the repository's own Apache-2.0 `LICENSE` and by
44
+ the copyright line in `NOTICE`.
45
+
46
+ All of the changes described above — the adapter, the heads, the calibration, the
47
+ serving binding and the serving layer — are Copyright 2026
48
+ Doccy Pty Ltd and licensed under Apache-2.0.
49
+
50
+ ## Third-party text scan
51
+
52
+ Before release, **every file staged into this repository was scanned for text
53
+ originating in third-party source documents.** The scan compared normalised
54
+ 6-gram and 8-gram shingles of every staged text file against:
55
+
56
+ 1. the 42 third-party source records the training and evaluation panels were
57
+ built from (Apache-2.0, MIT, BSD-3-Clause, CC BY 4.0, CC BY-SA 4.0, CC0,
58
+ OGL v3.0 and US-government public-domain assertions); and
59
+ 2. every generated panel and document corpus on disk.
60
+
61
+ **Result: no third-party document text is present in any shipped file.** The
62
+ only matches were:
63
+
64
+ * the reproduced Apache-2.0 licence text at `licenses/Qwen-Apache-2.0.txt`, which
65
+ matches an Apache-2.0 licence document held in the evaluation corpus and is
66
+ required to be here verbatim; and
67
+ * the phrase *"A missing fact is not a negative fact"*, which is **our own
68
+ prompt-template wording** appearing in our own evaluation panels, not
69
+ third-party text entering our prompts.
70
+
71
+ Acceptance fixtures are excluded from this repository entirely. The fixture
72
+ documents used in live acceptance are original synthetic text authored for this
73
+ project and held in tooling that is not distributed.
74
+
75
+ Re-run the scan with `ops/solomon_package.py --scan` in the source project.
mlx/bf16/NOTICE ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Solomon v1.1.0
2
+ Copyright 2026 Doccy Pty Ltd
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
5
+ this work except in compliance with the License. You may obtain a copy of the
6
+ License in the LICENSE file distributed with this work, or at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ --------------------------------------------------------------------------------
11
+ ATTRIBUTION FOR THE BASE MODEL (Apache License 2.0, section 4(c))
12
+ --------------------------------------------------------------------------------
13
+
14
+ This work is a DERIVATIVE WORK of:
15
+
16
+ Qwen/Qwen3.8-27B
17
+ Copyright 2026 Alibaba Cloud
18
+ Licensed under the Apache License, Version 2.0
19
+ Pinned revision: 1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0
20
+ Licence text: licenses/Qwen-Apache-2.0.txt (sha256 bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a, 11544 bytes,
21
+ the exact bytes served at the pinned revision)
22
+
23
+ The base model weights are NOT redistributed in this repository. They are
24
+ referenced by the pinned revision above and downloaded by the operator directly
25
+ from the upstream repository under the upstream licence.
26
+
27
+ Upstream NOTICE file: NONE. A NOTICE file is absent from Qwen/Qwen3.8-27B at the
28
+ pinned revision (HTTP 404, retrieved 2026-09-18T03:49Z without credentials).
29
+ Apache-2.0 section 4(d) therefore imposes no carry-forward obligation here. No
30
+ upstream attribution text has been invented or paraphrased.
31
+
32
+ "Qwen" and "Alibaba Cloud" are used nominatively, to identify the base model
33
+ this adapter was trained against. Apache-2.0 section 6 grants no trademark
34
+ rights and none are claimed or implied. Nothing here states or implies any
35
+ endorsement, sponsorship or affiliation.
36
+
37
+ --------------------------------------------------------------------------------
38
+ STATEMENT OF CHANGES (Apache License 2.0, section 4(b))
39
+ --------------------------------------------------------------------------------
40
+
41
+ No upstream source file is modified, and no upstream file is redistributed
42
+ except the unmodified licence text at licenses/Qwen-Apache-2.0.txt.
43
+
44
+ The modification this work carries is a trained LoRA adapter and a set of
45
+ trained linear answer heads, applied to the base model at inference time:
46
+
47
+ * LoRA adapter, rank 64, question-side placement, float32
48
+ adapter/adapter.safetensors
49
+ sha256 2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca
50
+ 870363376 bytes
51
+ * Trained linear answer heads
52
+ adapter/heads.npz
53
+ sha256 126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f
54
+ 2053938 bytes
55
+ * Readout calibration (one positive scalar per task)
56
+ serving/scope9-readout-temperature-v2.json
57
+ file sha256 baa7263ca9e865dda230617563e6a615de5050adc620c28924dffef68d527118
58
+ payload sha256 682d611ec53c3322905c7e265bf9b9b35a1cd581f6d8b7ea924df864a3d74278
59
+
60
+ Training took place between 16 and 20 September 2026. The adapter and the heads
61
+ are identified by the checksums above. Full change detail is in MODIFICATIONS.md.
62
+
63
+ --------------------------------------------------------------------------------
64
+ THIRD-PARTY CONTENT IN THIS REPOSITORY
65
+ --------------------------------------------------------------------------------
66
+
67
+ The only third-party content distributed here is the unmodified Apache License
68
+ 2.0 text at licenses/Qwen-Apache-2.0.txt, reproduced to satisfy section 4(a).
69
+
70
+ No training document, evaluation panel, dataset, corpus, rendered page, cached
71
+ state, score archive or acceptance fixture is distributed. Every file in this
72
+ repository was scanned for text originating in third-party source documents
73
+ before release; see MODIFICATIONS.md, "Third-party text scan".
74
+
75
+ Everything else in this repository -- the serving code under src/, the adapter
76
+ and head weights, the calibration artifact, the serving binding and the
77
+ documentation -- is original work of Doccy Pty Ltd, licensed under
78
+ Apache-2.0.
mlx/bf16/README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Solomon BF16 conversion provenance
2
+
3
+ This directory retains metadata for the unquantized BF16 conversion. **Backbone
4
+ weights are downloaded separately from Qwen; adapter and head weights live at
5
+ `adapter/adapter.safetensors` and `adapter/heads.npz` in the repository root.**
6
+ This directory is no longer a self-contained loadable model.
7
+
8
+ Install the source package in [`../`](../) and follow its README. From that
9
+ package directory:
10
+
11
+ ```sh
12
+ uv sync --frozen --extra dev
13
+ SOLOMON_COMMIT=$(uv run python -c 'from huggingface_hub import HfApi; print(HfApi().model_info("DoccyHealth/Solomon").sha)')
14
+ uv run solomon-mlx-hub prepare --revision "$SOLOMON_COMMIT" --output models/quality
15
+ ```
16
+
17
+ The loader fetches `adapter/**` plus MLX licensing metadata at the pinned Solomon
18
+ commit. It obtains the original base from `Qwen/Qwen3.8-27B` at
19
+ `1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0`, verifies every base file, and invokes
20
+ the included exact converter. Existing original weights can be supplied with
21
+ `--base`. Existing valid converted outputs are verified and reused.
22
+
23
+ The original converter source is `../src/solomon_mlx/prepare.py`. The runtime
24
+ source hash recorded in `conversion.json` for this historical conversion is
25
+ `648e440cface0838f2dcc8d89b3ab172d97f4ffc1f88fe0b7cbbe3ca73b8a575`. The
26
+ current runtime source hash is
27
+ `bbcae17fc1c35db80a79d5865133a42ef9a1b0cf342fff71949972e69cce43ec`;
28
+ the two differ only by the release documentation edits described in `../README.md`,
29
+ which changed no converter behaviour. The new Hub loader is packaged separately so the original
30
+ converter remains reproducible. Dependency versions are locked in `../uv.lock`.
31
+ Large matrices stay BF16; normalization parameters, `A_log`, and `dt_bias` stay
32
+ FP32, with normalization offsets applied after promotion. LoRA is never merged.
33
+
34
+ `conversion.json` and `binding.json` retain the historical output checksums and
35
+ paths, including removed backbone and duplicate adapter paths. The root adapter
36
+ and heads are byte-identical to those historical records. The old binding SHA-256
37
+ is `6d45715aa040fad98b061ff2cbafb35b047143ef2475646cee3df7a334f4d530`.
38
+ New local conversions receive a fresh binding with actual output checksums;
39
+ safetensors serialization may differ across CPU and Metal. The historical source
40
+ revision `2ec506902269e4636285c8811f6c0f52c9300c0c` is provenance, not a required
41
+ future download URL. After a history rewrite, pin the new repository commit.
42
+
43
+ **Experimental: complete CUDA parity remains pending.** Focused text/image
44
+ fixtures and a partial text panel match CUDA decisions. See the dated results in
45
+ `../docs/VALIDATION-20260921.md`. No new calibration or temperature fitting is
46
+ required or performed. Existing CUDA temperatures are applied identically to both
47
+ backends for comparison; CUDA qualification is not inherited.
48
+
49
+ The historical conversion produced 55,610,774,146 bytes. Focused inference used
50
+ about 59.9 GB peak Metal allocation on an M5 Max / 128 GB Mac; larger inputs need
51
+ more memory. This is full BF16, without quantization, and does not fit 24–32 GB Macs.
mlx/bf16/base-manifest.json ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "revision": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
3
+ "files": [
4
+ {
5
+ "name": ".gitattributes",
6
+ "size": 1570,
7
+ "blob_id": "52373fe24473b1aa44333d318f578ae6bf04b49b",
8
+ "sha256": null
9
+ },
10
+ {
11
+ "name": "LICENSE",
12
+ "size": 11544,
13
+ "blob_id": "f938136e3adacfd92be087f6e113b5d6d97f678f",
14
+ "sha256": null
15
+ },
16
+ {
17
+ "name": "README.md",
18
+ "size": 65012,
19
+ "blob_id": "bc8aa0e396cd029c21cd773cca21830d0ded28ec",
20
+ "sha256": null
21
+ },
22
+ {
23
+ "name": "chat_template.jinja",
24
+ "size": 8952,
25
+ "blob_id": "c0c686f9c38d70d179fb7b5f5aa7530bc913dda3",
26
+ "sha256": null
27
+ },
28
+ {
29
+ "name": "config.json",
30
+ "size": 4312,
31
+ "blob_id": "706cebd746c4b6f2b1d1f892630867acfdfd3df8",
32
+ "sha256": null
33
+ },
34
+ {
35
+ "name": "crc32.txt",
36
+ "size": 238,
37
+ "blob_id": "6de5ee6a0c6596744baee911af0a5cdcb8d99a1e",
38
+ "sha256": null
39
+ },
40
+ {
41
+ "name": "generation_config.json",
42
+ "size": 202,
43
+ "blob_id": "023756cfadf88e5bf69eefeee3e172f38c448d64",
44
+ "sha256": null
45
+ },
46
+ {
47
+ "name": "merges.txt",
48
+ "size": 3353259,
49
+ "blob_id": "a494e019ca1502219fd0128658b979e5f05ae8e8",
50
+ "sha256": null
51
+ },
52
+ {
53
+ "name": "model-00001-of-00018.safetensors",
54
+ "size": 3966730552,
55
+ "blob_id": "79b73cdd69afeca931d181f5d1e21ad2d73bb7b7",
56
+ "sha256": "ba0ce20aae489ad196733da5064bcdf159a1fe84f53336648196e1ebb7751b1c"
57
+ },
58
+ {
59
+ "name": "model-00002-of-00018.safetensors",
60
+ "size": 3043080328,
61
+ "blob_id": "81cf43545884a227a01351942dfbc72577812316",
62
+ "sha256": "06a148c01bfbe3faa14a5f184a7ff29a706f7ae1c8b2705d2058e26d17a001fb"
63
+ },
64
+ {
65
+ "name": "model-00003-of-00018.safetensors",
66
+ "size": 2542796952,
67
+ "blob_id": "a2da5e6cd6307a6cae13d04820ab0d31b47b48bd",
68
+ "sha256": "2e1bf62cbcd406eaa64b60d10353e1f0ef4039d0976e56f05cabe953454f9968"
69
+ },
70
+ {
71
+ "name": "model-00004-of-00018.safetensors",
72
+ "size": 3988973152,
73
+ "blob_id": "175ddab3f2488ad181f73d789d2d778cf6d4fca6",
74
+ "sha256": "511e34063187882659753c4d93f3859f93c019fd438d8813071921c81d9a3f1a"
75
+ },
76
+ {
77
+ "name": "model-00005-of-00018.safetensors",
78
+ "size": 2099339864,
79
+ "blob_id": "64890208fdd563c3aac0c2f55f69948bcaa1a492",
80
+ "sha256": "635cb53446dc74f219740fc59e18b774f877b803b9722e289ca62575a6efa701"
81
+ },
82
+ {
83
+ "name": "model-00006-of-00018.safetensors",
84
+ "size": 3979553696,
85
+ "blob_id": "3ae29b86e4c773babfe17124dbd10d54d37819ec",
86
+ "sha256": "0bc5214fac607f0e6cc92eec3789d4b8559410ef9fce66621ba8158e8410dae0"
87
+ },
88
+ {
89
+ "name": "model-00007-of-00018.safetensors",
90
+ "size": 2108759344,
91
+ "blob_id": "b7e9c0f038884839f3dad38fef7e75ee3362c24f",
92
+ "sha256": "80b0c49033e9a0d5762562aa12f4acdb7f54da586f3d0110f28c48d91cf07892"
93
+ },
94
+ {
95
+ "name": "model-00008-of-00018.safetensors",
96
+ "size": 3979553696,
97
+ "blob_id": "0b58cbc52714e1c683711be87e7c2e33fb10e7a0",
98
+ "sha256": "7192c5b66185d3592927daabee1cc19e6f6e0ce75988ee20e824b624765fda79"
99
+ },
100
+ {
101
+ "name": "model-00009-of-00018.safetensors",
102
+ "size": 2108759344,
103
+ "blob_id": "4fc0c6b28004d809225d0e115f30e9e11c2345dc",
104
+ "sha256": "af3c48cc37af44f3db6ae0579baf019180d48d9c527caa0a1f03ff85813a56d8"
105
+ },
106
+ {
107
+ "name": "model-00010-of-00018.safetensors",
108
+ "size": 3979553696,
109
+ "blob_id": "9ec693a17c788084116c51b4f34632670df77612",
110
+ "sha256": "163490a76f3bea3a40855b7efc04ce6d27afaf1a34f0bbde495b9491f76457c9"
111
+ },
112
+ {
113
+ "name": "model-00011-of-00018.safetensors",
114
+ "size": 2108759344,
115
+ "blob_id": "3c63baaed16d833432a919647af53ac76bb6b007",
116
+ "sha256": "5f3ae1b948aeee39da77aec558e8236cd65fe4d7cb7686a76bb007acc563c6d8"
117
+ },
118
+ {
119
+ "name": "model-00012-of-00018.safetensors",
120
+ "size": 3979553696,
121
+ "blob_id": "8568b440a4666ed1b4cda0b5d21496378304f326",
122
+ "sha256": "a3de1c7114677a8f5ac5c4892c90e8238ea5c1e2038c80e757dfc87c3902ca55"
123
+ },
124
+ {
125
+ "name": "model-00013-of-00018.safetensors",
126
+ "size": 2108759344,
127
+ "blob_id": "724083995a2257194321a3d00b36fd7d4bb7348d",
128
+ "sha256": "06ab79a41f74c9c5cb734816feb0c7fc364104b227165ee7391231e1155aa02a"
129
+ },
130
+ {
131
+ "name": "model-00014-of-00018.safetensors",
132
+ "size": 3979553696,
133
+ "blob_id": "a60b80b6510d7938d951fbddc76aaadd121c6268",
134
+ "sha256": "4138ed94603065ba884bbcadedb04d7718bb40117e85e6f5c6fc5b9c05b7a85b"
135
+ },
136
+ {
137
+ "name": "model-00015-of-00018.safetensors",
138
+ "size": 2108759344,
139
+ "blob_id": "7d4c34f3d716e1a47acff2f576b65b41a3ec4f3f",
140
+ "sha256": "69224e27b9de4e7dbf6fc936c6eaae08447bda3b80a6c31a871ab451173afd22"
141
+ },
142
+ {
143
+ "name": "model-00016-of-00018.safetensors",
144
+ "size": 3979564040,
145
+ "blob_id": "c67e3e42c9715298fa7f68c6b378bfb1d74290d0",
146
+ "sha256": "73cb9a1089fb6155cb648609478d6633be8a5c7d9ca5a05bc8925ce8a553cefe"
147
+ },
148
+ {
149
+ "name": "model-00017-of-00018.safetensors",
150
+ "size": 2108759344,
151
+ "blob_id": "23dd899179729a4370c655e2c2241cfdbe4ef4ba",
152
+ "sha256": "beb51f01056142ac4984bd800507b0dd0fd18de57f8e9ef6ea41d1a3598983a8"
153
+ },
154
+ {
155
+ "name": "model-00018-of-00018.safetensors",
156
+ "size": 3392197344,
157
+ "blob_id": "a92b4073fc6a58ffc9334ac63360fc8d9cc50d33",
158
+ "sha256": "1d3479509e21494658f9b64d317f5ea8e55c4025d28c702d6c4d0b356ce8ea06"
159
+ },
160
+ {
161
+ "name": "model.safetensors.index.json",
162
+ "size": 112216,
163
+ "blob_id": "da35e3c564457dface7d138f0b6cac284ff8958c",
164
+ "sha256": null
165
+ },
166
+ {
167
+ "name": "preprocessor_config.json",
168
+ "size": 390,
169
+ "blob_id": "2ea84a437d448ff71b08df68fdd949d5cc4ebb64",
170
+ "sha256": null
171
+ },
172
+ {
173
+ "name": "tokenizer.json",
174
+ "size": 12809320,
175
+ "blob_id": "9328ce9c41e80f6dd7bc2c66d8ae1fc93bf87440",
176
+ "sha256": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3"
177
+ },
178
+ {
179
+ "name": "tokenizer_config.json",
180
+ "size": 17928,
181
+ "blob_id": "5de744b3fca2129d7186979ae47c06be33903243",
182
+ "sha256": null
183
+ },
184
+ {
185
+ "name": "video_preprocessor_config.json",
186
+ "size": 385,
187
+ "blob_id": "3ba673a5ad7d4d13f54155ecd38b2a94a6dac8fe",
188
+ "sha256": null
189
+ },
190
+ {
191
+ "name": "vocab.json",
192
+ "size": 6722759,
193
+ "blob_id": "0aa0ce0658d60ac4a5d609f4eadb0e8e43514176",
194
+ "sha256": null
195
+ }
196
+ ]
197
+ }
mlx/bf16/binding.json ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "adapter_dtype": "float32",
3
+ "adapter_placement": "question",
4
+ "adapter_scale": 2.0,
5
+ "base_hashes": {
6
+ ".gitattributes": "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930",
7
+ "LICENSE": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a",
8
+ "README.md": "57e4bdb258ee1a7d2635c5174ebd4e56abe392505cdb5f8bbb356b0dc4293641",
9
+ "chat_template.jinja": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041",
10
+ "config.json": "191e0af232104ed8b65258cf3fb2b842e288008baca7633c11b82a1ac7203aab",
11
+ "crc32.txt": "b42dd291f5f98b05807e458f5b47849969a9b3326acbf0c97d28b05826740b83",
12
+ "generation_config.json": "e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e",
13
+ "merges.txt": "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d",
14
+ "model-00001-of-00018.safetensors": "ba0ce20aae489ad196733da5064bcdf159a1fe84f53336648196e1ebb7751b1c",
15
+ "model-00002-of-00018.safetensors": "06a148c01bfbe3faa14a5f184a7ff29a706f7ae1c8b2705d2058e26d17a001fb",
16
+ "model-00003-of-00018.safetensors": "2e1bf62cbcd406eaa64b60d10353e1f0ef4039d0976e56f05cabe953454f9968",
17
+ "model-00004-of-00018.safetensors": "511e34063187882659753c4d93f3859f93c019fd438d8813071921c81d9a3f1a",
18
+ "model-00005-of-00018.safetensors": "635cb53446dc74f219740fc59e18b774f877b803b9722e289ca62575a6efa701",
19
+ "model-00006-of-00018.safetensors": "0bc5214fac607f0e6cc92eec3789d4b8559410ef9fce66621ba8158e8410dae0",
20
+ "model-00007-of-00018.safetensors": "80b0c49033e9a0d5762562aa12f4acdb7f54da586f3d0110f28c48d91cf07892",
21
+ "model-00008-of-00018.safetensors": "7192c5b66185d3592927daabee1cc19e6f6e0ce75988ee20e824b624765fda79",
22
+ "model-00009-of-00018.safetensors": "af3c48cc37af44f3db6ae0579baf019180d48d9c527caa0a1f03ff85813a56d8",
23
+ "model-00010-of-00018.safetensors": "163490a76f3bea3a40855b7efc04ce6d27afaf1a34f0bbde495b9491f76457c9",
24
+ "model-00011-of-00018.safetensors": "5f3ae1b948aeee39da77aec558e8236cd65fe4d7cb7686a76bb007acc563c6d8",
25
+ "model-00012-of-00018.safetensors": "a3de1c7114677a8f5ac5c4892c90e8238ea5c1e2038c80e757dfc87c3902ca55",
26
+ "model-00013-of-00018.safetensors": "06ab79a41f74c9c5cb734816feb0c7fc364104b227165ee7391231e1155aa02a",
27
+ "model-00014-of-00018.safetensors": "4138ed94603065ba884bbcadedb04d7718bb40117e85e6f5c6fc5b9c05b7a85b",
28
+ "model-00015-of-00018.safetensors": "69224e27b9de4e7dbf6fc936c6eaae08447bda3b80a6c31a871ab451173afd22",
29
+ "model-00016-of-00018.safetensors": "73cb9a1089fb6155cb648609478d6633be8a5c7d9ca5a05bc8925ce8a553cefe",
30
+ "model-00017-of-00018.safetensors": "beb51f01056142ac4984bd800507b0dd0fd18de57f8e9ef6ea41d1a3598983a8",
31
+ "model-00018-of-00018.safetensors": "1d3479509e21494658f9b64d317f5ea8e55c4025d28c702d6c4d0b356ce8ea06",
32
+ "model.safetensors.index.json": "77042094076611b69791a610065f28b7013b8c621795fa86ddccc8bac7d1b9df",
33
+ "preprocessor_config.json": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516",
34
+ "tokenizer.json": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3",
35
+ "tokenizer_config.json": "b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27",
36
+ "video_preprocessor_config.json": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13",
37
+ "vocab.json": "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"
38
+ },
39
+ "base_revision": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
40
+ "conversion": "upstream Qwen3.5 sanitization, norms promoted before unit offset",
41
+ "dependencies": {
42
+ "mlx": "0.32.2",
43
+ "mlx-vlm": "0.7.1",
44
+ "numpy": "2.5.3",
45
+ "transformers": "5.17.0"
46
+ },
47
+ "dtype": "bfloat16",
48
+ "files": {
49
+ "LICENSE": "a61385b8bc4e786195136404b4975fede3c0bca526877f526482e9af93d47457",
50
+ "MODIFICATIONS.md": "218218b43b27132a6cfa032d2be908decd0966f5b655946a8668506731c77de7",
51
+ "NOTICE": "ccf2b8e98f0606945a5fcf9f6522c1f2c01b279d55dc21ee02c5a4acadf8e8d1",
52
+ "adapter.safetensors": "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca",
53
+ "backbone/.gitattributes": "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930",
54
+ "backbone/LICENSE": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a",
55
+ "backbone/README.md": "57e4bdb258ee1a7d2635c5174ebd4e56abe392505cdb5f8bbb356b0dc4293641",
56
+ "backbone/chat_template.jinja": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041",
57
+ "backbone/config.json": "191e0af232104ed8b65258cf3fb2b842e288008baca7633c11b82a1ac7203aab",
58
+ "backbone/crc32.txt": "b42dd291f5f98b05807e458f5b47849969a9b3326acbf0c97d28b05826740b83",
59
+ "backbone/generation_config.json": "e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e",
60
+ "backbone/merges.txt": "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d",
61
+ "backbone/model-00001-of-00018.safetensors": "6c54d794f08d8a479e8a123b6ab3473b8a8f304f0da59b043aabe7bdb5f05a7a",
62
+ "backbone/model-00002-of-00018.safetensors": "3de19e7456388362e7dbe1455362f6b2661d8d7084a7a346e7d3f77053ff0f78",
63
+ "backbone/model-00003-of-00018.safetensors": "103e7f75798c312e696d2547889732b110a634725f6233b7b0bc654bf3365528",
64
+ "backbone/model-00004-of-00018.safetensors": "9b6ff88af2b387c943366b263d1ebef06090cf2a94e637895a74eb10300bf5da",
65
+ "backbone/model-00005-of-00018.safetensors": "f2e95aa5371257fadde49c6d735b716aab00868ae689befd54c091df7a6c57b4",
66
+ "backbone/model-00006-of-00018.safetensors": "7c196564fbb98f683573ae02d3a90bd74ccb7289d9103f09c76d9feffcc64b24",
67
+ "backbone/model-00007-of-00018.safetensors": "1a657de97643d5283547047925453e0bbec151b5f293f0a48ee8fbffb82c83cb",
68
+ "backbone/model-00008-of-00018.safetensors": "e26030a4ee68739f4092696d685b9ef5b4a7052f4843e949cf4419ee7d086b81",
69
+ "backbone/model-00009-of-00018.safetensors": "d73885d7e8995d58cd722b6fd4976035f30e3464189a71d88bb8e504a4612c53",
70
+ "backbone/model-00010-of-00018.safetensors": "ee367c91f6faf3a2d5245e28ee20dbe4e51a1ab08f8e27029c67e7c47a77bd89",
71
+ "backbone/model-00011-of-00018.safetensors": "01ea6406c8ee3f3bd0cbe96fb1ff120bdafc20bdeb4eb5dc8fc08894fef9221f",
72
+ "backbone/model-00012-of-00018.safetensors": "59da9b95e4d818c1189f681044df6bb9a5c7375ea0a01b8d6a6a682451fc4a8e",
73
+ "backbone/model-00013-of-00018.safetensors": "c2f63ec1a168d0f328da8ef74caee72d4e67d6dccfd9ccb595abcaedaeae86c4",
74
+ "backbone/model-00014-of-00018.safetensors": "d8a6453ec4fbe9763644dd9127c3a5ad5fec5c8a5cde6f1414f1101ee9a1250c",
75
+ "backbone/model-00015-of-00018.safetensors": "95db368ab83b8f60d1b9205de6ffa97adbe665d7e61e9adcfb237d25dabf9f8a",
76
+ "backbone/model-00016-of-00018.safetensors": "482d3d948c8cb31068323a85b7c2d445271a28a08699bbed48c13e465894715a",
77
+ "backbone/model-00017-of-00018.safetensors": "7615db84bee928608723f0e5e887f98e11091ccd86583453efcf9745050741f2",
78
+ "backbone/model-00018-of-00018.safetensors": "1219c9ea7951f2bb93c0ccafa2e6e65ac9111fda00961e3fbe614e30050c95e9",
79
+ "backbone/model.safetensors.index.json": "5aec933c5ef08d6c1d411b1b1f225b97d7c55f8eeed4b74535849ce9361e595f",
80
+ "backbone/preprocessor_config.json": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516",
81
+ "backbone/tokenizer.json": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3",
82
+ "backbone/tokenizer_config.json": "b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27",
83
+ "backbone/video_preprocessor_config.json": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13",
84
+ "backbone/vocab.json": "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003",
85
+ "heads.npz": "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f"
86
+ },
87
+ "head_dtype": "float32",
88
+ "profile": "quality",
89
+ "quantization": null,
90
+ "recurrent_state_dtype": "float32",
91
+ "schema": "solomon-mlx-binding-v1",
92
+ "sensitive_parameters": "FP32 normalization weights, A_log and dt_bias",
93
+ "solomon_revision": "2ec506902269e4636285c8811f6c0f52c9300c0c",
94
+ "source_hashes": {
95
+ ".gitattributes": "8baff4be52225e26dd13dffb05fed3d0db821911c36d7ffd6f6d00d02ca421c7",
96
+ "LICENSE": "a61385b8bc4e786195136404b4975fede3c0bca526877f526482e9af93d47457",
97
+ "MANIFEST.json": "a473433af0c2de6a97a0d99e9e47216e91ed67c4269e07ca8d678d8c068c317b",
98
+ "MODIFICATIONS.md": "218218b43b27132a6cfa032d2be908decd0966f5b655946a8668506731c77de7",
99
+ "NOTICE": "ccf2b8e98f0606945a5fcf9f6522c1f2c01b279d55dc21ee02c5a4acadf8e8d1",
100
+ "PUSH.md": "cc8e0ea0a0dec3db020f8245eb90b04571b77d8ac9f3efcd64d0ab0bef5bac16",
101
+ "README.md": "51a2389ef504652f0b0a292cfaf28bb5e7635bde1aede0289f42696ad5818afc",
102
+ "adapter/adapter.safetensors": "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca",
103
+ "adapter/config.json": "cafc99ce8299eeca3fc2b2e1a2b08dcf659b66467d6675b6f8731a616693dbc4",
104
+ "adapter/heads.npz": "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f",
105
+ "licenses/Qwen-Apache-2.0.txt": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a",
106
+ "requirements.lock": "27675e999e5bcfc175ed768711856a975c2286d2ba807471cc7b0213d6d13f89",
107
+ "serving/scope9-readout-temperature-v2.json": "baa7263ca9e865dda230617563e6a615de5050adc620c28924dffef68d527118",
108
+ "serving/selection.json": "14fa9c88a913d61a60d13b65f0974cff3985f40d50f204a1b83a9426db21b362",
109
+ "serving/service-export.json": "ccd7c0ca37cd900912631ab98b72703a4a4aebf2fadd6208675781aabd1e5ac8",
110
+ "serving/serving-binding.json": "1849df4bd4897d048a45a2f4cbc63beac4ded6afc0d43ff41e53a8a6dc8c49e4",
111
+ "src/scope35/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
112
+ "src/scope35/engine.py": "61f91be0dabc680795e5d4b83f9d80f02cbed2502b06e3809fdc09fbeeb99202",
113
+ "src/scope4/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
114
+ "src/scope4/cuda_engine.py": "f41dd19a248193bbda6aa81705317c702e0ec07976c7520a513b14260edddbf2",
115
+ "src/scope4/cuda_engine_v2.py": "d536117abd403811bba62129c2ba30d2e890c9058741baff65f3e7414f829bce",
116
+ "src/scope4/cuda_service.py": "c1c2267401b88dc5d4ad3d18b08c547c9b2e5d0a01c922d8ba88a1756075315a",
117
+ "src/scope4/engine.py": "b1ef3525011a8ec66c308f461e525d73a0a2b927bbfa605c76bef960d25fb5ff",
118
+ "src/scope4/readout.py": "cafcadbfc6ab3e77eac7bcd092d2254049c987913e2f744318ae7e2f36c24587",
119
+ "src/scope4/service.py": "2dfbfbadf92bce3f001063f9d94e5bf19a8d0ff9d143dd68b20d5032dd2e5e73",
120
+ "src/scope5/__init__.py": "6c079cc4864d55e798e243a6cbdc6e2b3fd963ff31fd28cf9a8122819a8c65cf",
121
+ "src/scope5/cuda_engine.py": "2d7d64d7e01cccefdb7afd504b80c54443ca271d0bcd3cf352b6bfd99dc491e3",
122
+ "src/scope5/heads_runtime.py": "48c0c29c79a8b781cddf22034b9ffee3df9a8d61b8709b1dd808b8df47024fe7",
123
+ "src/scope5/heads_service.py": "5c775e26bcb2de1c32156ce0cc404d06de8fbedff6c67fcb5606ce3713b88342",
124
+ "src/scope5/routing.py": "0d88a7f0433dd448b81bd04c48044c98e453038d9ffcd903c9479c20ec6651c1",
125
+ "src/scope5/service.py": "d056ae3e4ebda5d89ba4ebe0c93ae225a31e33a8f543632a0976703c25501019",
126
+ "src/scope6/__init__.py": "e72ad8e334509eb002496207c4e43598d222ba2953162344e43ee122e3832df0",
127
+ "src/scope6/evidence.py": "a0ece458b5c0546c1f80b48cea87ebc1c3732e83f2c0f8e58969645bc8bde2f7",
128
+ "src/scope6/evidence_v3.py": "9b2b9e9c319242ca9c4a96a2567d1c1adb3134332c95dd6dfd60114c54d09426",
129
+ "src/scope6/retrieval.py": "4e5094aacc2ee3bbb0663de402198cd418933c4682f9d5a9006d4058e15f9b4f",
130
+ "src/scope6/runtime.py": "2d154d8ba39e61a10b23bba286e1ce87f57dbdc21d8f6e73430c0929a1990fe7",
131
+ "src/scope6/service.py": "5e7b6f9e163be35ab17ed76355ecee9faa541f27ca76e969ff49f2076dfbd9b8",
132
+ "src/scope6/service_v3.py": "b92cfdee8ca1c039645a97746e47fc99965f9c5f5ecd98e31aedf71bca29b611",
133
+ "src/scope6/serving.py": "aab481eb4b1d34c13caaf8c3628f3702f2956bb413f288c7a30979d5c2281356",
134
+ "src/scope9/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
135
+ "src/scope9/api.py": "db677e569a3f7d15e6398d4bfb9ac0b00f286502fe4d97e9811388ccfb95dcae",
136
+ "src/scope9/live.py": "62f40568c8c0bc7ea4d436325eb54d49fe06ea8f51c304b384dcffb605bf3e3f",
137
+ "src/scope9/prompts.py": "53e22b99c19f5af588fea2167594c6331877a8ce71d0a0424bce10000bc22b53",
138
+ "src/scope9/semantics.py": "9fd9041c50f8925608bf6a2f351abab745648137400475bdf46f66885bef67dc",
139
+ "src/scope9/service.py": "9deed4e6acae4b26619a95f6f2ef19ced6bb42d6d6a4e1710c26accff4bc198b"
140
+ }
141
+ }
mlx/bf16/conversion.json ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "run_id": "bf16-cpu-79517a5cd7aa",
3
+ "base_revision": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
4
+ "solomon_revision": "2ec506902269e4636285c8811f6c0f52c9300c0c",
5
+ "backend": "mlx-linux-cpu",
6
+ "binding_sha256": "6d45715aa040fad98b061ff2cbafb35b047143ef2475646cee3df7a334f4d530",
7
+ "converter_code_sha256": "648e440cface0838f2dcc8d89b3ab172d97f4ffc1f88fe0b7cbbe3ca73b8a575",
8
+ "total_output_bytes": 55610774146,
9
+ "seconds": 220.91851106299998,
10
+ "apple_silicon_validation": "pending; conversion is not inference qualification",
11
+ "files": [
12
+ {
13
+ "name": "LICENSE",
14
+ "size": 11344,
15
+ "sha256": "a61385b8bc4e786195136404b4975fede3c0bca526877f526482e9af93d47457"
16
+ },
17
+ {
18
+ "name": "MODIFICATIONS.md",
19
+ "size": 4197,
20
+ "sha256": "218218b43b27132a6cfa032d2be908decd0966f5b655946a8668506731c77de7"
21
+ },
22
+ {
23
+ "name": "NOTICE",
24
+ "size": 3891,
25
+ "sha256": "ccf2b8e98f0606945a5fcf9f6522c1f2c01b279d55dc21ee02c5a4acadf8e8d1"
26
+ },
27
+ {
28
+ "name": "adapter.safetensors",
29
+ "size": 870363376,
30
+ "sha256": "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca"
31
+ },
32
+ {
33
+ "name": "backbone/.gitattributes",
34
+ "size": 1570,
35
+ "sha256": "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930"
36
+ },
37
+ {
38
+ "name": "backbone/LICENSE",
39
+ "size": 11544,
40
+ "sha256": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a"
41
+ },
42
+ {
43
+ "name": "backbone/README.md",
44
+ "size": 65012,
45
+ "sha256": "57e4bdb258ee1a7d2635c5174ebd4e56abe392505cdb5f8bbb356b0dc4293641"
46
+ },
47
+ {
48
+ "name": "backbone/chat_template.jinja",
49
+ "size": 8952,
50
+ "sha256": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041"
51
+ },
52
+ {
53
+ "name": "backbone/config.json",
54
+ "size": 4312,
55
+ "sha256": "191e0af232104ed8b65258cf3fb2b842e288008baca7633c11b82a1ac7203aab"
56
+ },
57
+ {
58
+ "name": "backbone/crc32.txt",
59
+ "size": 238,
60
+ "sha256": "b42dd291f5f98b05807e458f5b47849969a9b3326acbf0c97d28b05826740b83"
61
+ },
62
+ {
63
+ "name": "backbone/generation_config.json",
64
+ "size": 202,
65
+ "sha256": "e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e"
66
+ },
67
+ {
68
+ "name": "backbone/merges.txt",
69
+ "size": 3353259,
70
+ "sha256": "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d"
71
+ },
72
+ {
73
+ "name": "backbone/model-00001-of-00018.safetensors",
74
+ "size": 3967078356,
75
+ "sha256": "6c54d794f08d8a479e8a123b6ab3473b8a8f304f0da59b043aabe7bdb5f05a7a"
76
+ },
77
+ {
78
+ "name": "backbone/model-00002-of-00018.safetensors",
79
+ "size": 3043154160,
80
+ "sha256": "3de19e7456388362e7dbe1455362f6b2661d8d7084a7a346e7d3f77053ff0f78"
81
+ },
82
+ {
83
+ "name": "backbone/model-00003-of-00018.safetensors",
84
+ "size": 2542796953,
85
+ "sha256": "103e7f75798c312e696d2547889732b110a634725f6233b7b0bc654bf3365528"
86
+ },
87
+ {
88
+ "name": "backbone/model-00004-of-00018.safetensors",
89
+ "size": 3989088609,
90
+ "sha256": "9b6ff88af2b387c943366b263d1ebef06090cf2a94e637895a74eb10300bf5da"
91
+ },
92
+ {
93
+ "name": "backbone/model-00005-of-00018.safetensors",
94
+ "size": 2099392965,
95
+ "sha256": "f2e95aa5371257fadde49c6d735b716aab00868ae689befd54c091df7a6c57b4"
96
+ },
97
+ {
98
+ "name": "backbone/model-00006-of-00018.safetensors",
99
+ "size": 3979669611,
100
+ "sha256": "7c196564fbb98f683573ae02d3a90bd74ccb7289d9103f09c76d9feffcc64b24"
101
+ },
102
+ {
103
+ "name": "backbone/model-00007-of-00018.safetensors",
104
+ "size": 2108811989,
105
+ "sha256": "1a657de97643d5283547047925453e0bbec151b5f293f0a48ee8fbffb82c83cb"
106
+ },
107
+ {
108
+ "name": "backbone/model-00008-of-00018.safetensors",
109
+ "size": 3979669575,
110
+ "sha256": "e26030a4ee68739f4092696d685b9ef5b4a7052f4843e949cf4419ee7d086b81"
111
+ },
112
+ {
113
+ "name": "backbone/model-00009-of-00018.safetensors",
114
+ "size": 2108812007,
115
+ "sha256": "d73885d7e8995d58cd722b6fd4976035f30e3464189a71d88bb8e504a4612c53"
116
+ },
117
+ {
118
+ "name": "backbone/model-00010-of-00018.safetensors",
119
+ "size": 3979669601,
120
+ "sha256": "ee367c91f6faf3a2d5245e28ee20dbe4e51a1ab08f8e27029c67e7c47a77bd89"
121
+ },
122
+ {
123
+ "name": "backbone/model-00011-of-00018.safetensors",
124
+ "size": 2108812003,
125
+ "sha256": "01ea6406c8ee3f3bd0cbe96fb1ff120bdafc20bdeb4eb5dc8fc08894fef9221f"
126
+ },
127
+ {
128
+ "name": "backbone/model-00012-of-00018.safetensors",
129
+ "size": 3979669597,
130
+ "sha256": "59da9b95e4d818c1189f681044df6bb9a5c7375ea0a01b8d6a6a682451fc4a8e"
131
+ },
132
+ {
133
+ "name": "backbone/model-00013-of-00018.safetensors",
134
+ "size": 2108811987,
135
+ "sha256": "c2f63ec1a168d0f328da8ef74caee72d4e67d6dccfd9ccb595abcaedaeae86c4"
136
+ },
137
+ {
138
+ "name": "backbone/model-00014-of-00018.safetensors",
139
+ "size": 3979669605,
140
+ "sha256": "d8a6453ec4fbe9763644dd9127c3a5ad5fec5c8a5cde6f1414f1101ee9a1250c"
141
+ },
142
+ {
143
+ "name": "backbone/model-00015-of-00018.safetensors",
144
+ "size": 2108812005,
145
+ "sha256": "95db368ab83b8f60d1b9205de6ffa97adbe665d7e61e9adcfb237d25dabf9f8a"
146
+ },
147
+ {
148
+ "name": "backbone/model-00016-of-00018.safetensors",
149
+ "size": 3979690166,
150
+ "sha256": "482d3d948c8cb31068323a85b7c2d445271a28a08699bbed48c13e465894715a"
151
+ },
152
+ {
153
+ "name": "backbone/model-00017-of-00018.safetensors",
154
+ "size": 2108812005,
155
+ "sha256": "7615db84bee928608723f0e5e887f98e11091ccd86583453efcf9745050741f2"
156
+ },
157
+ {
158
+ "name": "backbone/model-00018-of-00018.safetensors",
159
+ "size": 2542796942,
160
+ "sha256": "1219c9ea7951f2bb93c0ccafa2e6e65ac9111fda00961e3fbe614e30050c95e9"
161
+ },
162
+ {
163
+ "name": "backbone/model.safetensors.index.json",
164
+ "size": 111087,
165
+ "sha256": "5aec933c5ef08d6c1d411b1b1f225b97d7c55f8eeed4b74535849ce9361e595f"
166
+ },
167
+ {
168
+ "name": "backbone/preprocessor_config.json",
169
+ "size": 390,
170
+ "sha256": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516"
171
+ },
172
+ {
173
+ "name": "backbone/tokenizer.json",
174
+ "size": 12809320,
175
+ "sha256": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3"
176
+ },
177
+ {
178
+ "name": "backbone/tokenizer_config.json",
179
+ "size": 17928,
180
+ "sha256": "b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27"
181
+ },
182
+ {
183
+ "name": "backbone/video_preprocessor_config.json",
184
+ "size": 385,
185
+ "sha256": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13"
186
+ },
187
+ {
188
+ "name": "backbone/vocab.json",
189
+ "size": 6722759,
190
+ "sha256": "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"
191
+ },
192
+ {
193
+ "name": "heads.npz",
194
+ "size": 2053938,
195
+ "sha256": "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f"
196
+ },
197
+ {
198
+ "name": "binding.json",
199
+ "size": 12306,
200
+ "sha256": "6d45715aa040fad98b061ff2cbafb35b047143ef2475646cee3df7a334f4d530"
201
+ }
202
+ ]
203
+ }
mlx/docs/UPSTREAM-MODIFICATIONS.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Statement of changes
2
+
3
+ Apache License 2.0, section 4(b): prominent notice that files carry modifications.
4
+
5
+ ## What is modified
6
+
7
+ **No upstream source file is modified.** No file from `Qwen/Qwen3.8-27B` is edited,
8
+ renamed, patched or redistributed in this repository, with one exception: the
9
+ upstream licence text is reproduced byte-for-byte at `licenses/Qwen-Apache-2.0.txt`
10
+ (sha256 `bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a`) because section 4(a) requires it. That file is
11
+ unmodified and carries the upstream copyright.
12
+
13
+ The modification this work carries is not an edit to a source file. It is a set
14
+ of **trained parameters applied to the base model at inference time**, plus an
15
+ original serving layer that reads the model's logits. Concretely:
16
+
17
+ | Change | Artifact | sha256 |
18
+ |---|---|---|
19
+ | LoRA adapter, rank 64, question-side placement, float32 | `adapter/adapter.safetensors` | `2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca` |
20
+ | Trained linear answer heads | `adapter/heads.npz` | `126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f` |
21
+ | Readout calibration, one positive scalar per task | `serving/scope9-readout-temperature-v2.json` | file `baa7263ca9e865dda230617563e6a615de5050adc620c28924dffef68d527118` / payload `682d611ec53c3322905c7e265bf9b9b35a1cd581f6d8b7ea924df864a3d74278` |
22
+ | Runtime identity binding | `serving/serving-binding.json` | payload `95683c1f87ec3f71b7657669dc311918ff53b7d41a981eaa8d827047e72cb505` |
23
+ | Serving layer (original work, not derived from upstream code) | `src/` | see `MANIFEST.json` |
24
+
25
+ Base model, unmodified and not redistributed: `Qwen/Qwen3.8-27B` at revision
26
+ `1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0`, Copyright 2026 Alibaba Cloud, Apache-2.0.
27
+
28
+ Training took place between 16 and 20 September 2026. The shipped artifacts are
29
+ identified by the checksums in the table above and in `MANIFEST.json`.
30
+
31
+ ## Which files carry a change notice
32
+
33
+ | File | Why |
34
+ |---|---|
35
+ | `NOTICE` | Section 4(b) and 4(c) statement, carried with every copy |
36
+ | `MODIFICATIONS.md` | this file |
37
+ | `README.md` | names the base model, the pinned revision and the derivative relationship in the front matter and in the body |
38
+ | `adapter/config.json` | machine-readable record of the base repo, the pinned revision and both weight checksums |
39
+ | `serving/serving-binding.json` | pins the exact runtime the weights were measured on and refuses to load against a different one |
40
+
41
+ No file under `src/` carries an upstream change notice, because no file under
42
+ `src/` contains upstream code. Every file there is original work, written for
43
+ this project, and is covered by the repository's own Apache-2.0 `LICENSE` and by
44
+ the copyright line in `NOTICE`.
45
+
46
+ All of the changes described above — the adapter, the heads, the calibration, the
47
+ serving binding and the serving layer — are Copyright 2026
48
+ Doccy Pty Ltd and licensed under Apache-2.0.
49
+
50
+ ## Third-party text scan
51
+
52
+ Before release, **every file staged into this repository was scanned for text
53
+ originating in third-party source documents.** The scan compared normalised
54
+ 6-gram and 8-gram shingles of every staged text file against:
55
+
56
+ 1. the 42 third-party source records the training and evaluation panels were
57
+ built from (Apache-2.0, MIT, BSD-3-Clause, CC BY 4.0, CC BY-SA 4.0, CC0,
58
+ OGL v3.0 and US-government public-domain assertions); and
59
+ 2. every generated panel and document corpus on disk.
60
+
61
+ **Result: no third-party document text is present in any shipped file.** The
62
+ only matches were:
63
+
64
+ * the reproduced Apache-2.0 licence text at `licenses/Qwen-Apache-2.0.txt`, which
65
+ matches an Apache-2.0 licence document held in the evaluation corpus and is
66
+ required to be here verbatim; and
67
+ * the phrase *"A missing fact is not a negative fact"*, which is **our own
68
+ prompt-template wording** appearing in our own evaluation panels, not
69
+ third-party text entering our prompts.
70
+
71
+ Acceptance fixtures are excluded from this repository entirely. The fixture
72
+ documents used in live acceptance are original synthetic text authored for this
73
+ project and held in tooling that is not distributed.
74
+
75
+ Re-run the scan with `ops/solomon_package.py --scan` in the source project.
mlx/docs/VALIDATION-20260921.md ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Solomon BF16 CUDA parity validation — 21 September 2026
2
+
3
+ Full BF16 inference was exercised on the Apple M5 Max / 128 GB Mac. No weights or temperatures were changed. New temperature fitting is out of scope.
4
+
5
+ ## Confirmed results
6
+
7
+ | Check | Result |
8
+ |---|---|
9
+ | Fresh text fixtures, all five answer types | 22/22 decisions match CUDA |
10
+ | Fresh page-image fixtures | 4/4 decisions match CUDA |
11
+ | Saved text subset | 645/645 branch decisions and 327/327 whole-question decisions match CUDA |
12
+ | Prefix tokens and captured full token sequences | Exact match with CUDA |
13
+ | Replay and fresh process versus previous run | Zero logit drift on this Mac |
14
+ | Public API cache isolation, evidence and invalid-input checks | Passed |
15
+ | Existing package tests | 21 passed |
16
+ | Parity workflow and reserved-answer regression tests | 2 passed |
17
+ | Lint | Passed |
18
+
19
+ The saved subset covers 196 documents and all five tasks. It comes from previously scored calibration-fit inputs and is a diagnostic subset, not a complete held-out qualification. It was used only for comparison; nothing was fitted.
20
+
21
+ ## Numerical differences
22
+
23
+ With the same frozen CUDA serving temperatures applied once on both backends, maximum probability drift on the saved subset is 2.5871 percentage points; mean drift is 0.0126 points. At identical T=1, maximum drift is 4.1508 points and mean drift is 0.0078 points. All compared decisions still agree.
24
+
25
+ CUDA and MLX logits are not bitwise equal. The report preserves the largest probability differences for inspection. No claim is made that the CUDA confidence calibration has been independently validated for MLX.
26
+
27
+ The focused runs used 59.90 GB peak Metal allocation. This is not a measurement of the maximum supported context or many-page memory use.
28
+
29
+ ## Remaining work
30
+
31
+ The untouched certification panel is now scored directly: 1,200 documents / 21,718 branches. It skips fit/development scoring and temperature fitting. The complete agreement gate is at least 99.9% for both branches and whole questions, at T=1 and at the frozen CUDA temperatures. Probability drift and task accuracy are reported separately.
32
+
33
+ Image fixtures and API evidence checks are limited tests; broad image accuracy and CUDA evidence-selection equivalence are not established by them.
34
+
35
+ Runtime fingerprint: `33c9b63f036c5c039a2db4ba944f5b0042ae8b8e485e68c2aeddb57c407f8908`.
36
+
37
+ Detailed evaluation inputs and outputs remain private; the source release includes aggregate results only.
38
+
39
+ ## Adapter-only packaging checks
40
+
41
+ The 21 September source update passed 24 portable package tests, including seven
42
+ Hub-loader tests. The local development suite passed 30 tests. The new verifier
43
+ also checked all 37 files of the existing full BF16 conversion. The built wheel
44
+ contains the original converter with its exact recorded source hash and both
45
+ pinned-input manifests. These checks cover packaging and integrity, not additional
46
+ trained-model parity.
mlx/examples/decide.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from solomon_mlx import Solomon
2
+
3
+ model = Solomon.load("models/quality")
4
+ questions = {
5
+ "certified": {"type": "noul", "instructions": "Is Rookwood Ltd certified?"},
6
+ "auditor": {
7
+ "type": "choice",
8
+ "instructions": "Who performs the audit?",
9
+ "options": ["The Buyer", "The grower", "An independent auditor"],
10
+ },
11
+ "severity": {
12
+ "type": "score",
13
+ "instructions": "What is the breach severity?",
14
+ "levels": ["none recorded", "minor", "material"],
15
+ },
16
+ }
17
+ with model.prefill(
18
+ "Rookwood Ltd is certified. An independent auditor performs the audit. One minor breach is recorded."
19
+ ) as state:
20
+ print(model.decide(state=state, questions=questions, evidence="support"))
21
+ state.save("document-replay.json")
mlx/pyproject.toml ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["hatchling==1.32.3"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "solomon-mlx"
7
+ version = "0.1.0"
8
+ description = "Solomon v1.1 BF16 inference on Apple Silicon"
9
+ license = "Apache-2.0"
10
+ license-files = ["LICENSE", "NOTICE", "MODIFICATIONS.md", "docs/UPSTREAM-MODIFICATIONS.md"]
11
+ requires-python = ">=3.12,<3.14"
12
+ dependencies = ["mlx==0.32.2", "mlx-vlm==0.7.1", "transformers==5.17.0", "numpy==2.5.3", "pillow==12.3.0", "safetensors==0.8.0", "huggingface-hub==1.32.0", "scipy==1.18.1"]
13
+
14
+ [project.optional-dependencies]
15
+ dev = ["pytest==9.1.1", "ruff==0.16.8", "modal==1.5.5"]
16
+ cloud = [
17
+ "boto3==1.43.98",
18
+ "mlx[cpu]==0.32.2; sys_platform == 'linux'",
19
+ "modal==1.5.5",
20
+ ]
21
+
22
+ [project.scripts]
23
+ solomon-mlx = "solomon_mlx.cli:main"
24
+ solomon-mlx-hub = "solomon_mlx_hub.__main__:main"
25
+
26
+ [tool.pytest.ini_options]
27
+ testpaths = ["tests"]
28
+ markers = ["model: requires the full pinned model"]
29
+
30
+ [tool.ruff]
31
+ line-length = 110
32
+ extend-exclude = ["src/solomon_mlx/_vendor", "snapshots", "models", "evaluations"]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/solomon_mlx", "src/solomon_mlx_hub"]
mlx/requirements.cloud.lock ADDED
@@ -0,0 +1,1072 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv export --frozen --no-dev --extra cloud --no-emit-project --format requirements-txt --output-file requirements.cloud.lock
3
+ aiohappyeyeballs==2.7.1 \
4
+ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \
5
+ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472
6
+ # via aiohttp
7
+ aiohttp==3.14.3 \
8
+ --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \
9
+ --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \
10
+ --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \
11
+ --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \
12
+ --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \
13
+ --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \
14
+ --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \
15
+ --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \
16
+ --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \
17
+ --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \
18
+ --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \
19
+ --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \
20
+ --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \
21
+ --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \
22
+ --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \
23
+ --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \
24
+ --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \
25
+ --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \
26
+ --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \
27
+ --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \
28
+ --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \
29
+ --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \
30
+ --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \
31
+ --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \
32
+ --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \
33
+ --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \
34
+ --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \
35
+ --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \
36
+ --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \
37
+ --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \
38
+ --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \
39
+ --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \
40
+ --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \
41
+ --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \
42
+ --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \
43
+ --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \
44
+ --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \
45
+ --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \
46
+ --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \
47
+ --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \
48
+ --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \
49
+ --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24
50
+ # via modal
51
+ aiosignal==1.4.0 \
52
+ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
53
+ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
54
+ # via aiohttp
55
+ annotated-doc==0.0.5 \
56
+ --hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \
57
+ --hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb
58
+ # via
59
+ # fastapi
60
+ # typer
61
+ annotated-types==0.8.0 \
62
+ --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
63
+ --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
64
+ # via pydantic
65
+ anyio==4.15.1 \
66
+ --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \
67
+ --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94
68
+ # via
69
+ # httpx
70
+ # starlette
71
+ # watchfiles
72
+ attrs==26.1.0 \
73
+ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
74
+ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
75
+ # via aiohttp
76
+ boto3==1.43.98 \
77
+ --hash=sha256:1ec732e023fb29c12dc8520f925b5bbbed29eeb36b5764b5a5c26052d7c721f7 \
78
+ --hash=sha256:7454f666a9e852a56db0a7fa23be33a899f64e27828930eac43c3edba7b34e17
79
+ # via solomon-mlx
80
+ botocore==1.43.98 \
81
+ --hash=sha256:6135dd639ea6d1b3b49381bc253c8a139d61f7d44cb5f7dae8e7cd1791758572 \
82
+ --hash=sha256:84b35b10402c2fc0c265f634fbf86336eecc6489ee23f55074b329924b2cfd6f
83
+ # via
84
+ # boto3
85
+ # s3transfer
86
+ cbor2==6.1.4 \
87
+ --hash=sha256:01ecc79a28f33d17331943ce508fc1e21f4b06553c73f874f4c77120d72b2ef9 \
88
+ --hash=sha256:1fc15061553e4494dc10883237501e3402c645fe509248dd698e1faf2460d68b \
89
+ --hash=sha256:2310f07db3f9ba26f2a623774ff9f3dc7185af54f732ea119785a6b1bf7e1e7e \
90
+ --hash=sha256:310f3dfb296ba48fe9b63c5cf26e691e3548a1eae6901d2f0c18e941d151f220 \
91
+ --hash=sha256:32a4663425fbca4a4a7aa918eb5789d844c406439e58424cf34511f79f559242 \
92
+ --hash=sha256:36ae16d64b1f7b620c1af748e7b6947e20069ef80eee56871c5fbb84cc635905 \
93
+ --hash=sha256:4bd29f21529e279d50fc14f1a811f7b05b4d8e66a7969163cce98983b6817245 \
94
+ --hash=sha256:553a46bda7d09552631a714e22b91e6ff2c867ecd91511596ce290d8879b8d5b \
95
+ --hash=sha256:598710183daae69cbdeb177a870ec64aa601de8138a61491fd256826d15a860f \
96
+ --hash=sha256:5e6c76004d674ad1c620660cb0bc5a8a0b72a5d8c7b70926d8e09e6d7e87332f \
97
+ --hash=sha256:69978901302ecbc8cda57b520487c5c5240ed217de783eb7728fceb258311d76 \
98
+ --hash=sha256:ad4efa23fee6447e56a269191044e06eb39e809458bcd674e164fe9445feafd0 \
99
+ --hash=sha256:c08b9c7d2ea013e24a0cb819b872b0119dde404f64a1182c0b24095b7bba781f \
100
+ --hash=sha256:c48a7c938fc5fa5300ff82b5df09068dcb4838685ae8556b5ee8279d74f97ab4 \
101
+ --hash=sha256:cc8cd300e236e9797b2e1ce306109dc481fcccf78bfa2682bf36d99e6eab1ec6 \
102
+ --hash=sha256:d2560c2ba6a95904ba2a0ca257af878c4344409d9b46d8e646d8ebb617b1e0dd \
103
+ --hash=sha256:d9ada5a6ccfbb8ea7a3aa2aeb028421b52d8e0cd9323f0a2aeaa9c09d25fbce2
104
+ # via modal
105
+ certifi==2026.7.22 \
106
+ --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
107
+ --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
108
+ # via
109
+ # httpcore
110
+ # httpx
111
+ # modal
112
+ # requests
113
+ cffi==2.1.1 \
114
+ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
115
+ --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \
116
+ --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \
117
+ --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \
118
+ --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \
119
+ --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \
120
+ --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \
121
+ --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \
122
+ --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \
123
+ --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \
124
+ --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \
125
+ --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \
126
+ --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \
127
+ --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \
128
+ --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \
129
+ --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \
130
+ --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \
131
+ --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \
132
+ --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \
133
+ --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \
134
+ --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \
135
+ --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
136
+ --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \
137
+ --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \
138
+ --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \
139
+ --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \
140
+ --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
141
+ # via
142
+ # miniaudio
143
+ # sounddevice
144
+ charset-normalizer==3.5.1 \
145
+ --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \
146
+ --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \
147
+ --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \
148
+ --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \
149
+ --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \
150
+ --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \
151
+ --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \
152
+ --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \
153
+ --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \
154
+ --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \
155
+ --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \
156
+ --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \
157
+ --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \
158
+ --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \
159
+ --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \
160
+ --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \
161
+ --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \
162
+ --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \
163
+ --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \
164
+ --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \
165
+ --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \
166
+ --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \
167
+ --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \
168
+ --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \
169
+ --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \
170
+ --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \
171
+ --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \
172
+ --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \
173
+ --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \
174
+ --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \
175
+ --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \
176
+ --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \
177
+ --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \
178
+ --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \
179
+ --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \
180
+ --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \
181
+ --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \
182
+ --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \
183
+ --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \
184
+ --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \
185
+ --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \
186
+ --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \
187
+ --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \
188
+ --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \
189
+ --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \
190
+ --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \
191
+ --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \
192
+ --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \
193
+ --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \
194
+ --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \
195
+ --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \
196
+ --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \
197
+ --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \
198
+ --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \
199
+ --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0
200
+ # via requests
201
+ click==8.5.0 \
202
+ --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \
203
+ --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34
204
+ # via
205
+ # huggingface-hub
206
+ # modal
207
+ # uvicorn
208
+ colorama==0.4.6 ; sys_platform == 'win32' \
209
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
210
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
211
+ # via
212
+ # tqdm
213
+ # typer
214
+ fastapi==0.141.1 \
215
+ --hash=sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3 \
216
+ --hash=sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1
217
+ # via mlx-vlm
218
+ filelock==4.0.1 \
219
+ --hash=sha256:481a321a27bef441e23c53371c6abc8d7d16e26b97090074ba44f7538a3fd55a \
220
+ --hash=sha256:fdefc3f3e87716d855ae2b732c1cfd521dd99799ef2b4d00e8c0d4dcdc7cc94b
221
+ # via huggingface-hub
222
+ frozenlist==1.8.0 \
223
+ --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \
224
+ --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \
225
+ --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \
226
+ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \
227
+ --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \
228
+ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \
229
+ --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \
230
+ --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \
231
+ --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \
232
+ --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \
233
+ --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \
234
+ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \
235
+ --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \
236
+ --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \
237
+ --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \
238
+ --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \
239
+ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \
240
+ --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \
241
+ --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \
242
+ --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \
243
+ --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \
244
+ --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \
245
+ --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \
246
+ --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \
247
+ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \
248
+ --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \
249
+ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \
250
+ --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \
251
+ --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \
252
+ --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \
253
+ --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \
254
+ --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \
255
+ --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \
256
+ --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \
257
+ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \
258
+ --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \
259
+ --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \
260
+ --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \
261
+ --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \
262
+ --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \
263
+ --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \
264
+ --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \
265
+ --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \
266
+ --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \
267
+ --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \
268
+ --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \
269
+ --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \
270
+ --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \
271
+ --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \
272
+ --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd
273
+ # via
274
+ # aiohttp
275
+ # aiosignal
276
+ fsspec==2026.9.0 \
277
+ --hash=sha256:0f08147951c8cb31d844c3547d631053b127863b60be04cf06e121333ee0e2fe \
278
+ --hash=sha256:8dd6e646e99ea382bd85f97a45e6b526a442d79423a7dc673f1e2756d05fcb5f
279
+ # via huggingface-hub
280
+ grpclib==0.4.9 \
281
+ --hash=sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e \
282
+ --hash=sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46
283
+ # via modal
284
+ h11==0.16.0 \
285
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
286
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
287
+ # via
288
+ # httpcore
289
+ # uvicorn
290
+ h2==4.4.1 \
291
+ --hash=sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6 \
292
+ --hash=sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516
293
+ # via grpclib
294
+ hf-xet==1.6.0 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' \
295
+ --hash=sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7 \
296
+ --hash=sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef \
297
+ --hash=sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3 \
298
+ --hash=sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb \
299
+ --hash=sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d \
300
+ --hash=sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c \
301
+ --hash=sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f \
302
+ --hash=sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a \
303
+ --hash=sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b
304
+ # via huggingface-hub
305
+ hpack==4.2.0 \
306
+ --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \
307
+ --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986
308
+ # via h2
309
+ httpcore==1.0.9 \
310
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
311
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
312
+ # via httpx
313
+ httpx==0.28.1 \
314
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
315
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
316
+ # via huggingface-hub
317
+ huggingface-hub==1.32.0 \
318
+ --hash=sha256:b0c7c80561969d9cdacdd55fce67ba9584cca0b9d4ea80957a3a5c1445fac5c8 \
319
+ --hash=sha256:ed70a45498abe86039df7c2f4e5f7575de524be908d3840e8f828d5525eafd6a
320
+ # via
321
+ # mlx-audio
322
+ # solomon-mlx
323
+ # tokenizers
324
+ # transformers
325
+ hyperframe==6.1.0 \
326
+ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \
327
+ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08
328
+ # via h2
329
+ idna==3.20 \
330
+ --hash=sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44 \
331
+ --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c
332
+ # via
333
+ # anyio
334
+ # httpx
335
+ # requests
336
+ # yarl
337
+ jinja2==3.1.6 \
338
+ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
339
+ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
340
+ # via mlx-vlm
341
+ jmespath==1.1.0 \
342
+ --hash=sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d \
343
+ --hash=sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64
344
+ # via
345
+ # boto3
346
+ # botocore
347
+ llguidance==1.8.0 \
348
+ --hash=sha256:020b4ec2254a20555e69095c7d488907f17d7e65ab5a840b267530f2cb369f70 \
349
+ --hash=sha256:0eb7be70bf822e54cd4021bb200cfe2b86e4f3379d251067dc9f7da327f3ceab \
350
+ --hash=sha256:18d1579eabb040e65c870d50c6df19a7bef140c5260d12ad35b7f0dc446312e0 \
351
+ --hash=sha256:39668c11396896e5f05f59b70c81e4afd060b3408f02c8518b7a6943bfbb8a5d \
352
+ --hash=sha256:6ae4343bd40b88d1dd824a17edcee11b5e5a000b16b6fedb9fcf7f58d019177c \
353
+ --hash=sha256:6bf3953d06e7f5e24bd02fa6a89a5b2b88f7e811c0fa0228487d8267ef7cec54 \
354
+ --hash=sha256:79b0576991b8fc7534456b65c41d43c3183c8ca974a17799359af969c1489c07 \
355
+ --hash=sha256:a8837ac2b3bf4c46e1b6363012a22de043b7f8ef013b04d8689d471eb573b766 \
356
+ --hash=sha256:b5e866d8a896e255f30ec952f5280c61a3d6f391a9dce575ce976dd58f0b7000 \
357
+ --hash=sha256:bb9a89e8cdd7c8b5cf4f84e45b04177e79acdcc4d5116fbc775e511f7314df44
358
+ # via mlx-vlm
359
+ markdown-it-py==4.2.0 \
360
+ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
361
+ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
362
+ # via rich
363
+ markupsafe==3.0.3 \
364
+ --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
365
+ --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
366
+ --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
367
+ --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
368
+ --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
369
+ --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
370
+ --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
371
+ --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
372
+ --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
373
+ --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
374
+ --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
375
+ --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
376
+ --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
377
+ --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
378
+ --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
379
+ --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
380
+ --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
381
+ --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
382
+ --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
383
+ --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
384
+ --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
385
+ --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
386
+ --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
387
+ --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
388
+ --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
389
+ --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
390
+ --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
391
+ --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
392
+ --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
393
+ --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
394
+ --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
395
+ --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
396
+ --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
397
+ --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795
398
+ # via jinja2
399
+ mdurl==0.1.2 \
400
+ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
401
+ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
402
+ # via markdown-it-py
403
+ miniaudio==1.71 \
404
+ --hash=sha256:12bc33e7e61072b4b541c14e10ef76119d5643e6bbb98e2dec0c0738889438fb \
405
+ --hash=sha256:19be6f0a1e601c2237433e579734cfaf6469191b224c20c9e5f73c32ef9ee2b9 \
406
+ --hash=sha256:1bf93aeede652926f27f430f0fd69ef0cf8a949c07b537d6a2f295602c747037 \
407
+ --hash=sha256:4c849ccb1349f7b3553a77a66fe7e972315185f5c4c44a0bbda7ebcdd224db37 \
408
+ --hash=sha256:61b86f26d653040db32d9d15b05446321dd10e45beba25b44f841e26935213d5 \
409
+ --hash=sha256:62db602651bc20a2698f36a0d356d7217ed6f4f917550c7ffb3705c8e8be90cf \
410
+ --hash=sha256:70fa2ea5353e6919aca59b8c5768144af009d18c3bca251749d66fb497424563 \
411
+ --hash=sha256:8fc1a4f084cc1b4b25c567d22f54d1e46bfa505c17ed777c8b198e5c53d0f785 \
412
+ --hash=sha256:ab100e5240b104b5326e4ec1be07b6ae461f7d3d4d7a694857fd2f0493d210f9 \
413
+ --hash=sha256:d9dc15eff711bcfc62a9d05e0c78e4bc34821a455595e049629f2fea7491a523 \
414
+ --hash=sha256:e6287f15caa808a88aad0700a182bec1ff6d98769717425adf9ebf41259d1936 \
415
+ --hash=sha256:f4a44b70b66628b0c307e40ae0ae857695978cae18462179b806d8edc807d416 \
416
+ --hash=sha256:ff51e2887bb673e2e757752b586b3dc924d59aa5fbcae9bbc45f4a111bd3262b
417
+ # via
418
+ # mlx-audio
419
+ # mlx-vlm
420
+ mlx==0.32.2 \
421
+ --hash=sha256:45857fadb381fea3db57d9681b04c987813fa883f3182ec9fb05bc56f468978a \
422
+ --hash=sha256:48e8738b078eeb7bfde74931fc02d8aa9c32f05d3c0dbf97b5c3cbe0470fbddf \
423
+ --hash=sha256:50ced716f4ab860cbda9d65adf74877923fe26036c4e28d8f572ecec57621cfc \
424
+ --hash=sha256:583111ec13fedf63ddbfefda77dabd57168978f06474f5e5d2637180d154fcc4 \
425
+ --hash=sha256:65beb9ce75153072808ef18913ecca3d929b87be5d7c1f57d9df391c04b6f957 \
426
+ --hash=sha256:65d3d29b66045ed8dd2d8e437c8770de325843c364f7b7c38cd8ae90a7eec854 \
427
+ --hash=sha256:68560fd648c5bb900aa6f6765cd74c5a8abaf092d97d73584a57b7545966c227 \
428
+ --hash=sha256:6c615ad1c6877d7d38affe8526446955cd48d6796e871150f59145cf0d1a265d \
429
+ --hash=sha256:77217798a2b036bae9f213b851d4cde4581893787c9964458b7d471f86036bd6 \
430
+ --hash=sha256:8d270ade1e48e006383a6b5f33a3a4ec5c265389299a60a60bf55165af1770d9 \
431
+ --hash=sha256:9d21abe340403b6bf445e8494590a7341b655739088c92f178d7bd4241ba0110 \
432
+ --hash=sha256:bc69bd1062b97028ae6b79522ed0bd635a2b133fe3c398c03b66b060c20d9240 \
433
+ --hash=sha256:c7670ffb854c11e6776349a3797fa076d1a2a00290ebe2e9cfed8b60ca4a5db0 \
434
+ --hash=sha256:c95a384de1a0c0ba18425344cad8ac87180e3c5c1921a42e2621475be5966bcf \
435
+ --hash=sha256:cf63fd5c32258ab07523b06401c20ee2280bf56e26e991d05bf6fd8a4d42d1f6 \
436
+ --hash=sha256:daebf84dfb857d70e87b1b98189a057e691e99a4a2b9f6f64cadebad917f8964 \
437
+ --hash=sha256:dc5eb3cc30d4285f2734c368442f717599e97294663441cb42959e3b856d6898 \
438
+ --hash=sha256:df8c75e509de868fca148dfeb38d92ce956eed386569c87caeb72bd16d2d6962 \
439
+ --hash=sha256:f6071e4973927966c12b3894deb75f4fcfc33300e0705312a80fdbbaa92a9c4f \
440
+ --hash=sha256:f77e47e6c1e176a61ec1bb1f9e74d874482eea6eb86ffb162a1f3e789c806453 \
441
+ --hash=sha256:fe813a4dee2daa6d5ac496ef25117034728c77fe26f713983a3b406eb6f4e8f0
442
+ # via
443
+ # mlx-audio
444
+ # mlx-vlm
445
+ # solomon-mlx
446
+ mlx-audio==0.5.4 \
447
+ --hash=sha256:3e1895860d9a636360a9377b5651239f5197e649d56fe25f1382cac6f2ed1e55 \
448
+ --hash=sha256:d350ecc43a65b94b1578381370be64229948f79aef9ff599d20adb65e38351b3
449
+ # via mlx-vlm
450
+ mlx-cpu==0.32.2 ; sys_platform == 'linux' \
451
+ --hash=sha256:d0f94625588b51a878786dd51cec5617894ce586cf3c94645ce1b64e24c27c0b \
452
+ --hash=sha256:fc5d31b90fc4f457b2f9614ed45651a567d22a6a11a03d114a90a4e10bdd8878
453
+ # via mlx
454
+ mlx-metal==0.32.2 ; sys_platform == 'darwin' \
455
+ --hash=sha256:3825fff379dbc107dd3413e564a06caeaa24819910ec49c0439e454c06a1b9b8 \
456
+ --hash=sha256:55a369250d220b2cf10213a87a2ac1b1a420608c5b35b1df4e7147ac8e32f121 \
457
+ --hash=sha256:e6abeac9ac5265830c9c1541b6f96e9be37a85c2446763a46ad466c63a3837ab
458
+ # via mlx
459
+ mlx-vlm==0.7.1 \
460
+ --hash=sha256:b8abd3cc7e3513d9915bf9f6903833c697dc4c3084c2acdc880029c33e331a03 \
461
+ --hash=sha256:d9696bc3a2e961f43b5948a101dc0c966b5989d382e6910c37e2f3d555bc3408
462
+ # via solomon-mlx
463
+ modal==1.5.5 \
464
+ --hash=sha256:30df363ed1898cc3d91a09ff3f95c38ab043f6b6294011b01085312c6a0ac777 \
465
+ --hash=sha256:8d10d3ee09818aaba1973b73ce2521ab8961b63a29b5b52e3ff0d25e7a74808e
466
+ # via solomon-mlx
467
+ multidict==6.9.0 \
468
+ --hash=sha256:0db5bf96ec2ce45a8bc7fbbe8a486089969bb2791a66b6789ee3aed0d5dd562e \
469
+ --hash=sha256:1126782e3c3b1a7ccd990be3d3221709348e4b07d6ecf8b50965a93ae2624145 \
470
+ --hash=sha256:11e32ccf23cdbfcf8299a6a825930a858ec9a6aa05d6752d0f90f2bdf19489e1 \
471
+ --hash=sha256:1eb7939025bd9289d9dfe3a399102b1642f4fbb105a6af82f284052ee89e97a9 \
472
+ --hash=sha256:22067e88ff266e6a5dc59114529332692a01cc04b10dbdc2c14ab91217dee819 \
473
+ --hash=sha256:254e53be2ec70518bb82dfa9b0c7166baaf2bd0e918bd17b65caaf489d5a5522 \
474
+ --hash=sha256:2ea72901860ccbe94517421681c60b13533ce03ba2f7bd96293c3a4d16ac4ccb \
475
+ --hash=sha256:3e78870909e9f9e3d672ba99f1eb75130d7e01c42e642e6a70f434df32ca0ee1 \
476
+ --hash=sha256:3ec1e387b1f8a85ae5b94aa8c4e0576912ffa4d31bd0578f24c950d4f05ee476 \
477
+ --hash=sha256:408fac672931f2458be3bc8c89d9facd16dac2aad17c7cee2ca1693eee99f07e \
478
+ --hash=sha256:4bb769ccc72e15d7d441e1a08f169d418376be77cdc387e813129c26b357fe50 \
479
+ --hash=sha256:4be612f23990a261060ccd8f15e89dad1f9b7bb0c1021c5c3987f5fc57959391 \
480
+ --hash=sha256:4bef8cb5edea9c9daeb8396a75eeb97f912c3fb3fa408fe659edfecf91e47424 \
481
+ --hash=sha256:516fa4817cd070088f616a901380db56189f86daee9da79aada3c9b653f49ed7 \
482
+ --hash=sha256:51a08dceed4b42ef25755ee2cbf50df325e90e6b415d00955ffdea2c394ad6c0 \
483
+ --hash=sha256:57c2445049f7d8e66306f712868219da7ff7168ef42263dc032401211bf1205c \
484
+ --hash=sha256:640113258c5925a9eed2c12523410b25565ac5df2fa6735fbae88fb09bcdd212 \
485
+ --hash=sha256:662315f8621b3559268813b11134c1f9633edd4ab5f7b713688a243c13f4026d \
486
+ --hash=sha256:67bcff396d2ad62197c95488a716a88462792b384b5ea6e44bf7c1070eddb8fc \
487
+ --hash=sha256:67bed23e9803945b0760650ec3e903af772c49abe869f3bc03c66b2e7649d5ef \
488
+ --hash=sha256:6a5111a2bd824c821a3dd09da29680391b0caaa18fea7761358f4001e6898d1c \
489
+ --hash=sha256:75f7fc21ffce9a792cb919f0e0b0b52117bd672f2a55d1929d573b6fc437f374 \
490
+ --hash=sha256:7aaa14f0b9ffa2780c5d3b21da0e9b58778ed47af3369df72e5fd6dc10ff8041 \
491
+ --hash=sha256:85ed0f3c3b01174aea5a8a8fcd456f64c2e9719c0f42c612c2694ecd35974a65 \
492
+ --hash=sha256:8bb6be697065cbf31051465f9939d65624573d2219c3da8316db9e7cf5e0f5a7 \
493
+ --hash=sha256:8e991677c4bdc5d9f2e71c74717a4e32cfe98930ca05becdf722b5eae1329d6a \
494
+ --hash=sha256:8f06c4da5315a6f709b13408c3e13f3b475f8c559ec7608c3c67062512871235 \
495
+ --hash=sha256:91092d597fcf0940cd6a59e64ba7156ee22d6899993fd7c5b1897fda71482f8f \
496
+ --hash=sha256:95d339c3b75b4a50c665bdcf8417428cd71c3e5cd48e194cb1d336fcb856beac \
497
+ --hash=sha256:97555ad30ad20a8eb90fa86522088eebbbd68aba03e56d53f4850b3535c65e61 \
498
+ --hash=sha256:9869105ab61db13004f9ed610cd29ce4237b2c609f5f7b8bb8f2339fb9dad17d \
499
+ --hash=sha256:b33e499a7b1f722547d57b4865ed68c03161643b9afe49fe097a0832078d183d \
500
+ --hash=sha256:bb69b724c345420ba49187a17a894f146099f5b2e501df42ddb4452ac8be37fa \
501
+ --hash=sha256:beda95a6bd0a2e2265b941b90ff72543afbeaebb6a450e3a6a010d10d4a2ffca \
502
+ --hash=sha256:c51fd8d59e72e45c64907bf7f81fbd94f4c8dcd1c8b2f23f4c2d7ede788fcfc3 \
503
+ --hash=sha256:c7ab60b91e11b25e7682c5cd8763fdd17929ea83f234ba441091f1492e631ea3 \
504
+ --hash=sha256:cb847cb4002e725f88ffe29883da8290fd0754fa0461ed8988735e20964a9f65 \
505
+ --hash=sha256:cff3cff5a725bdb8359962de8d7429aea592d7693dbd197eeabbdfab9b6300e9 \
506
+ --hash=sha256:d7d32c0543494efbc9394e2b571725071d08e295993486bc9a43f6f89375ee01 \
507
+ --hash=sha256:e231de8ce43d4fd10bec4f67f71238ed88a974de30a9f20be7a4ab16c970a988 \
508
+ --hash=sha256:e4826f6b56456fb1e98111d7bc20cbdc7fa0a41b1f9ad80ff2dd3f2f5b226fe1 \
509
+ --hash=sha256:e71a072c52c78b7f97cd4611df6cef10977e4f2367cd0654a7626192f931adde \
510
+ --hash=sha256:f93c9058a0eceac0df2ce9d4c8823b84786ee598194753c2ca0c100224405e47 \
511
+ --hash=sha256:fca5b74b5909c29041f857c40d51d9273636fb4221cf020e4c452deb1c448a40 \
512
+ --hash=sha256:fdd484b84d3394e805689c56be3ab1f877ae7ff0eb9ff90a3ee7a755cbebab4f
513
+ # via
514
+ # aiohttp
515
+ # grpclib
516
+ # yarl
517
+ numpy==2.5.3 \
518
+ --hash=sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def \
519
+ --hash=sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0 \
520
+ --hash=sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f \
521
+ --hash=sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff \
522
+ --hash=sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034 \
523
+ --hash=sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec \
524
+ --hash=sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58 \
525
+ --hash=sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204 \
526
+ --hash=sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e \
527
+ --hash=sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321 \
528
+ --hash=sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0 \
529
+ --hash=sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034 \
530
+ --hash=sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3 \
531
+ --hash=sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231 \
532
+ --hash=sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4 \
533
+ --hash=sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b \
534
+ --hash=sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9 \
535
+ --hash=sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034 \
536
+ --hash=sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958 \
537
+ --hash=sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563 \
538
+ --hash=sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c \
539
+ --hash=sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09 \
540
+ --hash=sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e
541
+ # via
542
+ # mlx-audio
543
+ # mlx-vlm
544
+ # opencv-python
545
+ # scipy
546
+ # solomon-mlx
547
+ # transformers
548
+ opencv-python==5.0.0.93 \
549
+ --hash=sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2 \
550
+ --hash=sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898 \
551
+ --hash=sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157 \
552
+ --hash=sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2 \
553
+ --hash=sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b \
554
+ --hash=sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039 \
555
+ --hash=sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac \
556
+ --hash=sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881 \
557
+ --hash=sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2
558
+ # via mlx-vlm
559
+ packaging==26.3 \
560
+ --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
561
+ --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
562
+ # via
563
+ # huggingface-hub
564
+ # transformers
565
+ pillow==12.3.0 \
566
+ --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \
567
+ --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \
568
+ --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \
569
+ --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \
570
+ --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \
571
+ --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \
572
+ --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \
573
+ --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \
574
+ --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \
575
+ --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \
576
+ --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \
577
+ --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \
578
+ --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \
579
+ --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \
580
+ --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \
581
+ --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \
582
+ --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \
583
+ --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \
584
+ --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \
585
+ --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \
586
+ --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \
587
+ --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7
588
+ # via
589
+ # mlx-vlm
590
+ # solomon-mlx
591
+ propcache==0.5.4 \
592
+ --hash=sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77 \
593
+ --hash=sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b \
594
+ --hash=sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba \
595
+ --hash=sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25 \
596
+ --hash=sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c \
597
+ --hash=sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111 \
598
+ --hash=sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc \
599
+ --hash=sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef \
600
+ --hash=sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468 \
601
+ --hash=sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177 \
602
+ --hash=sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab \
603
+ --hash=sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47 \
604
+ --hash=sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944 \
605
+ --hash=sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21 \
606
+ --hash=sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737 \
607
+ --hash=sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161 \
608
+ --hash=sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9 \
609
+ --hash=sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb \
610
+ --hash=sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526 \
611
+ --hash=sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546 \
612
+ --hash=sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1 \
613
+ --hash=sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f \
614
+ --hash=sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894 \
615
+ --hash=sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429 \
616
+ --hash=sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8 \
617
+ --hash=sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c \
618
+ --hash=sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8 \
619
+ --hash=sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867 \
620
+ --hash=sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937 \
621
+ --hash=sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031 \
622
+ --hash=sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9 \
623
+ --hash=sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c \
624
+ --hash=sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8 \
625
+ --hash=sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0 \
626
+ --hash=sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572 \
627
+ --hash=sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558
628
+ # via
629
+ # aiohttp
630
+ # yarl
631
+ protobuf==6.33.6 \
632
+ --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \
633
+ --hash=sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901 \
634
+ --hash=sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3 \
635
+ --hash=sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a \
636
+ --hash=sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135 \
637
+ --hash=sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3 \
638
+ --hash=sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2 \
639
+ --hash=sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593
640
+ # via modal
641
+ pycparser==3.0 ; implementation_name != 'PyPy' \
642
+ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
643
+ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
644
+ # via cffi
645
+ pydantic==2.13.5 \
646
+ --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 \
647
+ --hash=sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08
648
+ # via fastapi
649
+ pydantic-core==2.46.5 \
650
+ --hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \
651
+ --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 \
652
+ --hash=sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc \
653
+ --hash=sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5 \
654
+ --hash=sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0 \
655
+ --hash=sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931 \
656
+ --hash=sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038 \
657
+ --hash=sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b \
658
+ --hash=sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433 \
659
+ --hash=sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29 \
660
+ --hash=sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b \
661
+ --hash=sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8 \
662
+ --hash=sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e \
663
+ --hash=sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1 \
664
+ --hash=sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f \
665
+ --hash=sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761 \
666
+ --hash=sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869 \
667
+ --hash=sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5 \
668
+ --hash=sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c \
669
+ --hash=sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea \
670
+ --hash=sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519 \
671
+ --hash=sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e \
672
+ --hash=sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0 \
673
+ --hash=sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4 \
674
+ --hash=sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d \
675
+ --hash=sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f \
676
+ --hash=sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5 \
677
+ --hash=sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575 \
678
+ --hash=sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62 \
679
+ --hash=sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a \
680
+ --hash=sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed \
681
+ --hash=sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f \
682
+ --hash=sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3 \
683
+ --hash=sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff \
684
+ --hash=sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084
685
+ # via pydantic
686
+ pygments==2.21.0 \
687
+ --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
688
+ --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
689
+ # via rich
690
+ python-dateutil==2.9.0.post0 \
691
+ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
692
+ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
693
+ # via botocore
694
+ python-multipart==0.0.32 \
695
+ --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \
696
+ --hash=sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23
697
+ # via mlx-vlm
698
+ pyyaml==6.0.3 \
699
+ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
700
+ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
701
+ --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
702
+ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
703
+ --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
704
+ --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
705
+ --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
706
+ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
707
+ --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
708
+ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
709
+ --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
710
+ --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
711
+ --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
712
+ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
713
+ --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
714
+ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
715
+ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
716
+ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
717
+ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
718
+ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
719
+ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
720
+ # via
721
+ # huggingface-hub
722
+ # transformers
723
+ regex==2026.9.10 \
724
+ --hash=sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4 \
725
+ --hash=sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0 \
726
+ --hash=sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8 \
727
+ --hash=sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0 \
728
+ --hash=sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d \
729
+ --hash=sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4 \
730
+ --hash=sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348 \
731
+ --hash=sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd \
732
+ --hash=sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1 \
733
+ --hash=sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0 \
734
+ --hash=sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7 \
735
+ --hash=sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1 \
736
+ --hash=sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86 \
737
+ --hash=sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502 \
738
+ --hash=sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383 \
739
+ --hash=sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643 \
740
+ --hash=sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406 \
741
+ --hash=sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5 \
742
+ --hash=sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb \
743
+ --hash=sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db \
744
+ --hash=sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a \
745
+ --hash=sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041 \
746
+ --hash=sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3 \
747
+ --hash=sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944 \
748
+ --hash=sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7 \
749
+ --hash=sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae \
750
+ --hash=sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23 \
751
+ --hash=sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd \
752
+ --hash=sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da \
753
+ --hash=sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4 \
754
+ --hash=sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b \
755
+ --hash=sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca \
756
+ --hash=sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1
757
+ # via transformers
758
+ requests==2.34.2 \
759
+ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
760
+ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
761
+ # via mlx-vlm
762
+ rich==15.0.0 \
763
+ --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
764
+ --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
765
+ # via
766
+ # modal
767
+ # typer
768
+ s3transfer==0.19.2 \
769
+ --hash=sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993 \
770
+ --hash=sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25
771
+ # via boto3
772
+ safetensors==0.8.0 \
773
+ --hash=sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358 \
774
+ --hash=sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f \
775
+ --hash=sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d \
776
+ --hash=sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d \
777
+ --hash=sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0 \
778
+ --hash=sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc \
779
+ --hash=sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235 \
780
+ --hash=sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98 \
781
+ --hash=sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4 \
782
+ --hash=sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846 \
783
+ --hash=sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca \
784
+ --hash=sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0 \
785
+ --hash=sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25 \
786
+ --hash=sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452 \
787
+ --hash=sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d \
788
+ --hash=sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78 \
789
+ --hash=sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774
790
+ # via
791
+ # solomon-mlx
792
+ # transformers
793
+ scipy==1.18.1 \
794
+ --hash=sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3 \
795
+ --hash=sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89 \
796
+ --hash=sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1 \
797
+ --hash=sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307 \
798
+ --hash=sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28 \
799
+ --hash=sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2 \
800
+ --hash=sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174 \
801
+ --hash=sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66 \
802
+ --hash=sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12 \
803
+ --hash=sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314 \
804
+ --hash=sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899 \
805
+ --hash=sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf \
806
+ --hash=sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1 \
807
+ --hash=sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315 \
808
+ --hash=sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12 \
809
+ --hash=sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07 \
810
+ --hash=sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93 \
811
+ --hash=sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265 \
812
+ --hash=sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6 \
813
+ --hash=sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218 \
814
+ --hash=sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9
815
+ # via
816
+ # mlx-audio
817
+ # solomon-mlx
818
+ sentencepiece==0.2.2 \
819
+ --hash=sha256:1edb10e520e4bddf74d85b0f5ae74cc2d60c2b448885080bfb618bc2b3a49f6b \
820
+ --hash=sha256:201a8e0f55501a76e08dbf2c54bc45f4642b379271e89c667d517bfbc2191f2a \
821
+ --hash=sha256:38111ed1f79268f399c505028023d5eaaf0ab4e5eafceb709468b0d3323e7838 \
822
+ --hash=sha256:3ab3f1ae98970b5590e2209341522718900ba19bcc2c207ffaa6bd417ad960c5 \
823
+ --hash=sha256:3d2b5e824b5622038dc7b490897efe05ebbbb9e7350fc142f3ecc8789ef9bdf6 \
824
+ --hash=sha256:3ec27c152a1f1b24bc9168b55a5880f3c16e2334e697da6f55a1046a22405a3d \
825
+ --hash=sha256:4f0603267cd15b92b68c2c0e852a441507614b70dc7773659baa6b8c214a91fd \
826
+ --hash=sha256:59d6588712101ccfcae9b03692be3aaae1514c2078666d7b05f15ba3a702e41b \
827
+ --hash=sha256:64b656f025355cf8c51abe9fbe3848540756c6d7ca5e6791b1afa664bc24c7cb \
828
+ --hash=sha256:72b7825b331b1b7e7c45be2e674b3e3c65af608fa376bad2d851b20aaf0cdc78 \
829
+ --hash=sha256:74f0ee601047c0c12a783088b51be4e6214a62ecd9e02278c477433cd16e0ed9 \
830
+ --hash=sha256:76ff5814db72e7462dece042d7593cdf102b8ec82c2b1cc201a2add34ee3050d \
831
+ --hash=sha256:77c3ce990b23441e5ecfa5bce181fd6f408b564aeb6d7e1d1e7de9c5612501c8 \
832
+ --hash=sha256:7c6e7bf684dc12145bfa685d3060beaea55139134ba848289bee514ed42e7383 \
833
+ --hash=sha256:89625fb43765cccaa1443b9adb61f283e5fe4cb1536728205d06bada730caa53 \
834
+ --hash=sha256:8eed98514bffe5ecac37f493f91869c351fbb05629328bfdbc08502c6c094dc0 \
835
+ --hash=sha256:b23fe17779834d3c27aaf2edac9486d04cca1a7deb8f5facda35150ac6263a91 \
836
+ --hash=sha256:c8a168b040bc61681293f79a949b5d911c8e25086f4260285b8d97ab5f1195da \
837
+ --hash=sha256:cbce24284f51f71d10a42b7b9c964dcb9048b28f1c8e5db40bcbcb6f428cba6a \
838
+ --hash=sha256:d795c4ac689a57f9d4ba2288126ec7901d389ad5827d2f8b8533c883974fe563 \
839
+ --hash=sha256:f7c06c751c19d923435a54bff4f7e66e728fad160e8da28254f133abc9725820 \
840
+ --hash=sha256:fd523c4992041faa5c2b3cde62253d11a96c30d73a34afe48a486e8e2254cd1c
841
+ # via mlx-vlm
842
+ shellingham==1.5.4 \
843
+ --hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
844
+ --hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
845
+ # via typer
846
+ six==1.17.0 \
847
+ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
848
+ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
849
+ # via python-dateutil
850
+ sounddevice==0.5.6 \
851
+ --hash=sha256:7f4162f514f007b0bf25a3ccfed3f1705bc2ec311888a90232729eec4f57a4f4 \
852
+ --hash=sha256:8ec9fbfde2e32f020b167e348f3ab3bac6625a5f15af524d790108ac7147a410 \
853
+ --hash=sha256:b36b807eb02abd257198bf84b2af05e4fea199a9d2f0019014169c7136d45e9c \
854
+ --hash=sha256:c8ae19173e5f27f8c12d4b5eee2dbfe542cee125d591e663e0fb4dfb75246d45 \
855
+ --hash=sha256:de099612311ad81e55d31ccbd83f43ea6bf4d87b48f9b6ea55a1fbcde0eee4e0 \
856
+ --hash=sha256:e3aef00ad8b1d1740eb66d9a7671eab88a4d2b8fa4ab33498d742e63b65c309c
857
+ # via mlx-audio
858
+ starlette==1.6.0 \
859
+ --hash=sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c \
860
+ --hash=sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b
861
+ # via
862
+ # fastapi
863
+ # mlx-vlm
864
+ synchronicity==0.12.5 \
865
+ --hash=sha256:94d96b1d85698e3056b96a793b8c0949af6584e4a7d877fabdeb5385efe230aa \
866
+ --hash=sha256:fdbbb10d437bc08a6b0f814fc66fddd1b58ffed314533d42f1ab555801e781af
867
+ # via modal
868
+ tokenizers==0.23.2 \
869
+ --hash=sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78 \
870
+ --hash=sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde \
871
+ --hash=sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7 \
872
+ --hash=sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305 \
873
+ --hash=sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb \
874
+ --hash=sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718 \
875
+ --hash=sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5 \
876
+ --hash=sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac \
877
+ --hash=sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90 \
878
+ --hash=sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703 \
879
+ --hash=sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf \
880
+ --hash=sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2 \
881
+ --hash=sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef \
882
+ --hash=sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a \
883
+ --hash=sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa \
884
+ --hash=sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40 \
885
+ --hash=sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835
886
+ # via transformers
887
+ toml==0.10.2 \
888
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
889
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
890
+ # via modal
891
+ tqdm==4.70.1 \
892
+ --hash=sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73 \
893
+ --hash=sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4
894
+ # via
895
+ # huggingface-hub
896
+ # mlx-audio
897
+ # mlx-vlm
898
+ # transformers
899
+ transformers==5.17.0 \
900
+ --hash=sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801 \
901
+ --hash=sha256:a153be279169b55b92d8000bf4af294aed684503d091cca7804da2dd8a9de000
902
+ # via
903
+ # mlx-audio
904
+ # mlx-vlm
905
+ # solomon-mlx
906
+ typer==0.27.2 \
907
+ --hash=sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945 \
908
+ --hash=sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d
909
+ # via transformers
910
+ types-certifi==2021.10.8.3 \
911
+ --hash=sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f \
912
+ --hash=sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a
913
+ # via modal
914
+ types-toml==0.10.8.20260518 \
915
+ --hash=sha256:0e564ab05f6fde62a315b3b5a9b6624fda569399795d30a37e64705a70459303 \
916
+ --hash=sha256:80e10facd24fdeda9d5c672187d72be3ac284843788d67f5aae59e3e016db6fe
917
+ # via modal
918
+ typing-extensions==4.16.0 \
919
+ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
920
+ --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
921
+ # via
922
+ # aiohttp
923
+ # aiosignal
924
+ # anyio
925
+ # fastapi
926
+ # huggingface-hub
927
+ # modal
928
+ # pydantic
929
+ # pydantic-core
930
+ # starlette
931
+ # synchronicity
932
+ # typing-inspection
933
+ typing-inspection==0.4.4 \
934
+ --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \
935
+ --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147
936
+ # via
937
+ # fastapi
938
+ # pydantic
939
+ urllib3==2.8.0 \
940
+ --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \
941
+ --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63
942
+ # via
943
+ # botocore
944
+ # requests
945
+ uvicorn==0.53.0 \
946
+ --hash=sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e \
947
+ --hash=sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e
948
+ # via mlx-vlm
949
+ watchfiles==1.2.0 \
950
+ --hash=sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9 \
951
+ --hash=sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98 \
952
+ --hash=sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f \
953
+ --hash=sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427 \
954
+ --hash=sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4 \
955
+ --hash=sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa \
956
+ --hash=sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c \
957
+ --hash=sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906 \
958
+ --hash=sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c \
959
+ --hash=sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c \
960
+ --hash=sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077 \
961
+ --hash=sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01 \
962
+ --hash=sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9 \
963
+ --hash=sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658 \
964
+ --hash=sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0 \
965
+ --hash=sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8 \
966
+ --hash=sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898 \
967
+ --hash=sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44 \
968
+ --hash=sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5 \
969
+ --hash=sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc \
970
+ --hash=sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5 \
971
+ --hash=sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9 \
972
+ --hash=sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0 \
973
+ --hash=sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55 \
974
+ --hash=sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb \
975
+ --hash=sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0 \
976
+ --hash=sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26 \
977
+ --hash=sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3 \
978
+ --hash=sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838 \
979
+ --hash=sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71 \
980
+ --hash=sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d \
981
+ --hash=sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44 \
982
+ --hash=sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2 \
983
+ --hash=sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5 \
984
+ --hash=sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799 \
985
+ --hash=sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379 \
986
+ --hash=sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925 \
987
+ --hash=sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72 \
988
+ --hash=sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08 \
989
+ --hash=sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4
990
+ # via modal
991
+ websockets==17.1 \
992
+ --hash=sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3 \
993
+ --hash=sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f \
994
+ --hash=sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d \
995
+ --hash=sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6 \
996
+ --hash=sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391 \
997
+ --hash=sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c \
998
+ --hash=sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906 \
999
+ --hash=sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e \
1000
+ --hash=sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608 \
1001
+ --hash=sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c \
1002
+ --hash=sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad \
1003
+ --hash=sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780 \
1004
+ --hash=sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98 \
1005
+ --hash=sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11 \
1006
+ --hash=sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641 \
1007
+ --hash=sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec \
1008
+ --hash=sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090 \
1009
+ --hash=sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8 \
1010
+ --hash=sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3 \
1011
+ --hash=sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a \
1012
+ --hash=sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2 \
1013
+ --hash=sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb \
1014
+ --hash=sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc \
1015
+ --hash=sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab \
1016
+ --hash=sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74 \
1017
+ --hash=sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729 \
1018
+ --hash=sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce \
1019
+ --hash=sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c \
1020
+ --hash=sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a \
1021
+ --hash=sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51 \
1022
+ --hash=sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60 \
1023
+ --hash=sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435 \
1024
+ --hash=sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08 \
1025
+ --hash=sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0 \
1026
+ --hash=sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1 \
1027
+ --hash=sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067 \
1028
+ --hash=sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c \
1029
+ --hash=sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837 \
1030
+ --hash=sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23 \
1031
+ --hash=sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a \
1032
+ --hash=sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4 \
1033
+ --hash=sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80
1034
+ # via mlx-vlm
1035
+ yarl==1.25.1 \
1036
+ --hash=sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7 \
1037
+ --hash=sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa \
1038
+ --hash=sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec \
1039
+ --hash=sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26 \
1040
+ --hash=sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651 \
1041
+ --hash=sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc \
1042
+ --hash=sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9 \
1043
+ --hash=sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5 \
1044
+ --hash=sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a \
1045
+ --hash=sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b \
1046
+ --hash=sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c \
1047
+ --hash=sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184 \
1048
+ --hash=sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58 \
1049
+ --hash=sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3 \
1050
+ --hash=sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707 \
1051
+ --hash=sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03 \
1052
+ --hash=sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40 \
1053
+ --hash=sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54 \
1054
+ --hash=sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d \
1055
+ --hash=sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4 \
1056
+ --hash=sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a \
1057
+ --hash=sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df \
1058
+ --hash=sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68 \
1059
+ --hash=sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe \
1060
+ --hash=sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85 \
1061
+ --hash=sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab \
1062
+ --hash=sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d \
1063
+ --hash=sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1 \
1064
+ --hash=sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6 \
1065
+ --hash=sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3 \
1066
+ --hash=sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad \
1067
+ --hash=sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d \
1068
+ --hash=sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f \
1069
+ --hash=sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a \
1070
+ --hash=sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98 \
1071
+ --hash=sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc
1072
+ # via aiohttp
mlx/requirements.lock ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiohappyeyeballs==2.7.1
2
+ aiohttp==3.14.3
3
+ aiosignal==1.4.0
4
+ annotated-doc==0.0.5
5
+ annotated-types==0.8.0
6
+ anyio==4.15.1
7
+ attrs==26.1.0
8
+ cbor2==6.1.4
9
+ certifi==2026.7.22
10
+ cffi==2.1.1
11
+ charset-normalizer==3.5.1
12
+ click==8.5.0
13
+ fastapi==0.141.1
14
+ filelock==4.0.1
15
+ frozenlist==1.8.0
16
+ fsspec==2026.9.0
17
+ grpclib==0.4.9
18
+ h11==0.16.0
19
+ h2==4.4.1
20
+ hf-xet==1.6.0
21
+ hpack==4.2.0
22
+ httpcore==1.0.9
23
+ httpx==0.28.1
24
+ huggingface-hub==1.32.0
25
+ hyperframe==6.1.0
26
+ idna==3.20
27
+ iniconfig==2.3.0
28
+ jinja2==3.1.6
29
+ llguidance==1.8.0
30
+ markdown-it-py==4.2.0
31
+ markupsafe==3.0.3
32
+ mdurl==0.1.2
33
+ miniaudio==1.71
34
+ mlx==0.32.2
35
+ mlx-audio==0.5.4
36
+ mlx-metal==0.32.2
37
+ mlx-vlm==0.7.1
38
+ modal==1.5.5
39
+ multidict==6.9.0
40
+ numpy==2.5.3
41
+ opencv-python==5.0.0.93
42
+ packaging==26.3
43
+ pillow==12.3.0
44
+ pluggy==1.6.0
45
+ propcache==0.5.4
46
+ protobuf==6.33.6
47
+ pycparser==3.0
48
+ pydantic==2.13.5
49
+ pydantic-core==2.46.5
50
+ pygments==2.21.0
51
+ pytest==9.1.1
52
+ python-multipart==0.0.32
53
+ pyyaml==6.0.3
54
+ regex==2026.9.10
55
+ requests==2.34.2
56
+ rich==15.0.0
57
+ ruff==0.16.8
58
+ safetensors==0.8.0
59
+ scipy==1.18.1
60
+ sentencepiece==0.2.2
61
+ shellingham==1.5.4
62
+ -e .
63
+ sounddevice==0.5.6
64
+ starlette==1.6.0
65
+ synchronicity==0.12.5
66
+ tokenizers==0.23.2
67
+ toml==0.10.2
68
+ tqdm==4.70.1
69
+ transformers==5.17.0
70
+ typer==0.27.2
71
+ types-certifi==2021.10.8.3
72
+ types-toml==0.10.8.20260518
73
+ typing-extensions==4.16.0
74
+ typing-inspection==0.4.4
75
+ urllib3==2.8.0
76
+ uvicorn==0.53.0
77
+ watchfiles==1.2.0
78
+ websockets==17.1
79
+ yarl==1.25.1
mlx/scripts/benchmark.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Full-model parity and measurements on this Mac; emits no simulated hardware results."""
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import mlx.core as mx
9
+ import numpy as np
10
+
11
+ from solomon_mlx import Solomon
12
+ from solomon_mlx._vendor.semantics import listed_probs, p_yes
13
+ from solomon_mlx.artifacts import digest, sha256
14
+
15
+
16
+ def probabilities(job, row):
17
+ if job["task"] in ("boolean", "multilabel", "entity"):
18
+ p = p_yes(row["letter_logits"])
19
+ return np.array([p, 1 - p])
20
+ n = job["n"] - 2 if job["head_key"].endswith("choiceR") else job["n"]
21
+ return listed_probs(row["letter_logits"], n)
22
+
23
+
24
+ def run(model_dir, jobs_path, reference_path, output):
25
+ output = Path(output)
26
+ if output.exists():
27
+ raise FileExistsError("Benchmark outputs are immutable")
28
+ jobs = json.loads(Path(jobs_path).read_text())
29
+ reference = json.loads(Path(reference_path).read_text())
30
+ ref = {r["id"]: r for r in reference["rows"]}
31
+ if set(ref) != {j["id"] for j in jobs}:
32
+ raise ValueError("Benchmark and reference jobs differ")
33
+ mx.reset_peak_memory()
34
+ started = time.perf_counter()
35
+ model = Solomon.load(model_dir)
36
+ load_seconds = time.perf_counter() - started
37
+ states = {}
38
+ rows = []
39
+ prefills = []
40
+ try:
41
+ for job in jobs:
42
+ key = digest(job["parts"])
43
+ if key not in states:
44
+ state = model.prefill(job["parts"])
45
+ states[key] = state
46
+ prefills.append(
47
+ {
48
+ "document": key,
49
+ "tokens": state.prefix_tokens,
50
+ "seconds": state._data["prefill_seconds"],
51
+ "vision_seconds": state._data["vision_seconds"],
52
+ "cache_bytes": sum(c.nbytes for c in state._data["cache"]),
53
+ }
54
+ )
55
+ state = states[key]
56
+ row = model.engine.ask(
57
+ state._data,
58
+ job["block"],
59
+ job["n"],
60
+ job["head_key"],
61
+ execution=job.get("execution", "cached"),
62
+ taps=job.get("taps", []),
63
+ )
64
+ p, q = probabilities(job, row), probabilities(job, ref[job["id"]])
65
+ row.update(
66
+ id=job["id"],
67
+ decision_agrees=bool(p.argmax() == q.argmax()),
68
+ max_probability_drift=float(np.max(np.abs(p - q))),
69
+ max_logit_drift=float(
70
+ np.max(np.abs(np.array(row["letter_logits"]) - ref[job["id"]]["letter_logits"]))
71
+ ),
72
+ prefix_ids_exact=state._data["prefix_ids"] == ref[job["id"]]["prefix_ids"],
73
+ )
74
+ if "token_ids" in row:
75
+ row["token_ids_exact"] = row["token_ids"] == ref[job["id"]]["token_ids"]
76
+ if row.get("taps"):
77
+ row["layer_max_hidden_drift"] = {
78
+ k: float(np.max(np.abs(np.array(v) - ref[job["id"]]["taps"][k])))
79
+ for k, v in row["taps"].items()
80
+ }
81
+ rows.append(row)
82
+ print(job["id"], row["seconds"], row["decision_agrees"], flush=True)
83
+ # Replay checks real prefill and repeated question semantics on the same binding.
84
+ first = states[digest(jobs[0]["parts"])]
85
+ recipe = output.with_suffix(".replay.json")
86
+ first.save(recipe)
87
+ with model.replay(recipe) as restored:
88
+ b, n, h = jobs[0]["block"], jobs[0]["n"], jobs[0]["head_key"]
89
+ replay = model.engine.ask(restored._data, b, n, h)
90
+ replay_drift = float(np.max(np.abs(np.array(replay["letter_logits"]) - rows[0]["letter_logits"])))
91
+ finally:
92
+ for state in states.values():
93
+ state.close()
94
+ warm = [r["seconds"] for r in rows if r["reused_prefix_tokens"]]
95
+ report = {
96
+ "runtime": model.identity,
97
+ "device": mx.device_info(),
98
+ "jobs_sha256": sha256(jobs_path),
99
+ "reference_sha256": sha256(reference_path),
100
+ "load_seconds": load_seconds,
101
+ "prefills": prefills,
102
+ "rows": rows,
103
+ "warm_question_latency_median_seconds": float(np.median(warm)),
104
+ "questions_per_second": len(warm) / sum(warm),
105
+ "peak_metal_bytes": mx.get_peak_memory(),
106
+ "replay_max_logit_drift": replay_drift,
107
+ "decision_agreement": float(np.mean([r["decision_agrees"] for r in rows])),
108
+ "scope": "development parity fixtures; not held-out task accuracy or release qualification",
109
+ "calibration_status": "uncalibrated",
110
+ }
111
+ output.write_text(json.dumps(report, indent=2))
112
+ return report
113
+
114
+
115
+ if __name__ == "__main__":
116
+ p = argparse.ArgumentParser()
117
+ p.add_argument("--model", default="models/quality")
118
+ p.add_argument("--jobs", default="evaluations/golden-jobs.json")
119
+ p.add_argument("--reference", default="evaluations/bf16-reference-1789901869/report.json")
120
+ p.add_argument("--output", default="evaluations/bf16-text-benchmark.json")
121
+ a = p.parse_args()
122
+ run(a.model, a.jobs, a.reference, a.output)
mlx/scripts/benchmark_chunks.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure 512/1024/2048-token prefill on frozen original acceptance documents."""
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import mlx.core as mx
9
+
10
+ from solomon_mlx import Solomon
11
+
12
+ p = argparse.ArgumentParser()
13
+ p.add_argument("--model", default="models/quality")
14
+ p.add_argument("--output", default="evaluations/chunk-benchmark.json")
15
+ a = p.parse_args()
16
+ out = Path(a.output)
17
+ if out.exists():
18
+ raise FileExistsError("Use a new immutable benchmark output")
19
+ model = Solomon.load(a.model)
20
+ fixtures = json.loads(Path("evaluations/cuda-acceptance/input/documents.json").read_text())
21
+ results = []
22
+ for document, parts in fixtures["documents"].items():
23
+ for target in (1242, 2048):
24
+ text = "".join(p["text"] for p in parts)
25
+ while True:
26
+ rendered = model.engine.render([{"text": text}], "X")
27
+ end = rendered.rfind("\n\nX")
28
+ length = len(model.engine.t.encode(rendered[:end], add_special_tokens=False)) - 1
29
+ if length >= target:
30
+ break
31
+ text += fixtures["filler"]
32
+ for chunk in (512, 1024, 2048):
33
+ model.engine.chunk_size = chunk
34
+ mx.reset_peak_memory()
35
+ started = time.perf_counter()
36
+ with model.prefill(text) as state:
37
+ results.append(
38
+ {
39
+ "document": document,
40
+ "chunk": chunk,
41
+ "tokens": state.prefix_tokens,
42
+ "seconds": time.perf_counter() - started,
43
+ "peak_metal_bytes": mx.get_peak_memory(),
44
+ "cache_bytes": sum(c.nbytes for c in state._data["cache"]),
45
+ }
46
+ )
47
+ print(results[-1], flush=True)
48
+ out.write_text(
49
+ json.dumps({"runtime": model.identity, "measurements": results, "hardware_simulation": False}, indent=2)
50
+ )
mlx/scripts/check_cuda_parity.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare saved MLX scores with CUDA using identical, frozen temperatures.
2
+
3
+ No fitting, parameter selection, or changes to inference weights take place.
4
+ Partial panels are diagnostic only and can never qualify a release.
5
+ """
6
+
7
+ import argparse
8
+ import json
9
+ from collections import defaultdict
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+
14
+ from solomon_mlx._vendor.semantics import listed_probs, p_yes
15
+ from solomon_mlx.api import TASKS
16
+ from solomon_mlx.artifacts import digest, runtime_identity, sha256
17
+ from solomon_mlx.evaluation import compare_rows, load_panel, read_cuda_scores
18
+
19
+
20
+ def decision_probabilities(row, temperature):
21
+ """Parity includes every branch, even when its gold label is not a listed option."""
22
+ if row["task"] in ("boolean", "entity", "multilabel"):
23
+ p = p_yes(row["letter_logits"], temperature)
24
+ return np.array([1 - p, p])
25
+ width = row["n"] - 2 if row["head_key"].endswith("choiceR") else row["n"]
26
+ return listed_probs(row["letter_logits"], width, temperature)
27
+
28
+
29
+ def compare(panel, scores, cuda_directory, reference, output, *, allow_partial=False):
30
+ panel, scores, output = Path(panel), Path(scores), Path(output)
31
+ if output.exists():
32
+ raise FileExistsError("Parity reports are immutable")
33
+ jobs, manifest = load_panel(panel)
34
+ identity = json.loads((scores / "identity.json").read_text())
35
+ model_binding = json.loads(Path("models/quality/binding.json").read_text())
36
+ if identity["runtime"] != runtime_identity(model_binding):
37
+ raise ValueError("Scores belong to another MLX runtime")
38
+ if identity["panel_sha256"] != manifest["jobs_sha256"]:
39
+ raise ValueError("Scores belong to another panel")
40
+ groups = defaultdict(list)
41
+ for job in jobs:
42
+ groups[job["document_key"]].append(job)
43
+ rows, files = [], {}
44
+ for key, group in groups.items():
45
+ path = scores / (key + ".json")
46
+ if not path.exists() and allow_partial:
47
+ continue
48
+ record = json.loads(path.read_text())
49
+ body = {k: v for k, v in record.items() if k != "sha256"}
50
+ if (
51
+ record["sha256"] != digest(body)
52
+ or record["identity"] != digest(identity)
53
+ or [r["id"] for r in record["rows"]] != [r["id"] for r in group]
54
+ ):
55
+ raise ValueError("Corrupt or mismatched score document")
56
+ rows.extend(record["rows"])
57
+ files[path.name] = sha256(path)
58
+ complete = len(files) == len(groups)
59
+ if not allow_partial:
60
+ marker = json.loads((scores / "complete.json").read_text())
61
+ if marker != {
62
+ "identity": digest(identity),
63
+ "documents": len(groups),
64
+ "branches": len(jobs),
65
+ "files": files,
66
+ }:
67
+ raise ValueError("Incomplete or mismatched completion manifest")
68
+ ref = json.loads(Path(reference).read_text())
69
+ cuda = read_cuda_scores(cuda_directory, panel, ref["identity"])
70
+ selected = {r["id"] for r in rows}
71
+ cuda = [r for r in cuda if r["id"] in selected]
72
+ binding_path = Path("evaluations/cuda-acceptance/input/serving-binding.json")
73
+ source_manifest = json.loads((binding_path.parent / "manifest.json").read_text())
74
+ if sha256(binding_path) != source_manifest["files"][binding_path.name]:
75
+ raise ValueError("CUDA acceptance binding checksum mismatch")
76
+ binding = json.loads(binding_path.read_text())
77
+ for key in (
78
+ "adapter_sha256",
79
+ "trained_heads_sha256",
80
+ "model_sha256",
81
+ "numerics",
82
+ "placement",
83
+ "arithmetic",
84
+ ):
85
+ if binding["runtime"][key] != ref["identity"][key]:
86
+ raise ValueError("CUDA temperatures belong to another reference")
87
+ temperatures = {task: binding["temperatures"]["models"][task]["temperature"] for task in TASKS}
88
+ comparisons = {}
89
+ cuda_by_id = {r["id"]: r for r in cuda}
90
+ for name, temps in (("temperature_one", dict.fromkeys(TASKS, 1.0)), ("cuda_serving", temperatures)):
91
+ result = compare_rows(rows, cuda, temperatures=temps, reference_temperatures=temps)
92
+ result.pop("quality_gate_passed")
93
+ result["accuracy_units"] = result["units"]
94
+ result["accuracy_questions"] = result["questions"]
95
+ worst, questions = [], defaultdict(list)
96
+ for row in rows:
97
+ other = {**row, "letter_logits": cuda_by_id[row["id"]]["letter_logits"]}
98
+ p = decision_probabilities(row, temps[row["task"]])
99
+ q = decision_probabilities(other, temps[row["task"]])
100
+ if not np.isfinite(p).all() or not np.isfinite(q).all():
101
+ raise ValueError("Nonfinite parity probability")
102
+ agrees = int(np.argmax(p)) == int(np.argmax(q))
103
+ questions[row["question_id"]].append(agrees)
104
+ worst.append(
105
+ {
106
+ "id": row["id"],
107
+ "task": row["task"],
108
+ "max_probability_drift": float(np.max(np.abs(p - q))),
109
+ "decision_agrees": agrees,
110
+ }
111
+ )
112
+ result.update(
113
+ units=len(rows),
114
+ questions=len(questions),
115
+ unit_decision_agreement=float(np.mean([r["decision_agrees"] for r in worst])),
116
+ question_decision_agreement=float(np.mean([all(v) for v in questions.values()])),
117
+ max_probability_drift=max(r["max_probability_drift"] for r in worst),
118
+ mean_probability_drift=float(np.mean([r["max_probability_drift"] for r in worst])),
119
+ )
120
+ result["agreement_gate_passed"] = (
121
+ result["unit_decision_agreement"] >= 0.999 and result["question_decision_agreement"] >= 0.999
122
+ )
123
+ result["largest_probability_differences"] = sorted(
124
+ worst, key=lambda r: r["max_probability_drift"], reverse=True
125
+ )[:10]
126
+ comparisons[name] = result
127
+ report = {
128
+ "scope": "complete text parity panel" if complete else "partial text parity diagnostic",
129
+ "complete": complete,
130
+ "documents": len(files),
131
+ "total_documents": len(groups),
132
+ "branches": len(rows),
133
+ "total_branches": len(jobs),
134
+ "tasks": sorted({r["task"] for r in rows}),
135
+ "runtime": identity["runtime"],
136
+ "cuda_runtime": ref["identity"],
137
+ "panel_sha256": identity["panel_sha256"],
138
+ "score_files_sha256": digest(files),
139
+ "cuda_serving_binding_sha256": sha256(binding_path),
140
+ "temperature_fitting_performed": False,
141
+ "temperature_policy": "identical settings on both backends; not MLX calibration",
142
+ "comparisons": comparisons,
143
+ "parity_gate_passed": complete and all(r["agreement_gate_passed"] for r in comparisons.values()),
144
+ "bitwise_equality_claimed": False,
145
+ "image_qualification": False,
146
+ }
147
+ output.parent.mkdir(parents=True, exist_ok=True)
148
+ output.write_text(json.dumps(report, indent=2))
149
+ print(
150
+ json.dumps(
151
+ {k: report[k] for k in ("scope", "documents", "branches", "tasks", "parity_gate_passed")},
152
+ indent=2,
153
+ )
154
+ )
155
+ return report
156
+
157
+
158
+ if __name__ == "__main__":
159
+ parser = argparse.ArgumentParser()
160
+ for field in ("panel", "scores", "cuda-directory", "output"):
161
+ parser.add_argument("--" + field, required=True)
162
+ parser.add_argument("--reference", default="evaluations/bf16-reference-1789901869/report.json")
163
+ parser.add_argument("--allow-partial", action="store_true")
164
+ args = parser.parse_args()
165
+ compare(
166
+ args.panel,
167
+ args.scores,
168
+ args.cuda_directory,
169
+ args.reference,
170
+ args.output,
171
+ allow_partial=args.allow_partial,
172
+ )
mlx/scripts/score_parity.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Score a frozen panel for CUDA parity without fitting temperatures."""
2
+
3
+ import argparse
4
+
5
+ from solomon_mlx import Solomon
6
+ from solomon_mlx.evaluation import score_panel
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description=__doc__)
11
+ parser.add_argument("--model", default="models/quality")
12
+ parser.add_argument("--panel", required=True)
13
+ parser.add_argument("--output", required=True)
14
+ args = parser.parse_args()
15
+ score_panel(Solomon.load(args.model), args.panel, args.output)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
mlx/scripts/validate_api.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Focused public API checks using the real BF16 model, without calibration."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+
9
+ from solomon_mlx import Solomon
10
+
11
+
12
+ def validate(output):
13
+ output = Path(output)
14
+ if output.exists():
15
+ raise FileExistsError("Validation outputs are immutable")
16
+ model = Solomon.load("models/quality")
17
+ document = "Alice is certified. Bob is not certified. The current priority is high."
18
+ questions = {
19
+ "boolean": "Is Alice certified?",
20
+ "single": {"type": "choice", "instructions": "Who is certified?", "options": ["Alice", "Bob"]},
21
+ "ordered": {"type": "score", "instructions": "What is the priority?", "levels": ["low", "high"]},
22
+ "entity": {"instructions": "Is {candidate} certified?", "candidates": ["Alice", "Bob"]},
23
+ "multilabel": {
24
+ "instructions": "Which facts apply?",
25
+ "candidates": ["Alice is certified", "Bob is certified"],
26
+ },
27
+ }
28
+ checks = {}
29
+ with model.prefill(document) as state:
30
+ first = model.decide(state=state, questions=questions, evidence="none", diagnostics=True)
31
+ assert set(first["answers"]) == set(questions)
32
+ checks["all_five_answer_types"] = True
33
+ repeated = model.decide(
34
+ state=state, questions={"boolean": questions["boolean"]}, evidence="none", diagnostics=True
35
+ )
36
+ a = first["answers"]["boolean"]["branches"][0]["letter_logits"]
37
+ b = repeated["answers"]["boolean"]["branches"][0]["letter_logits"]
38
+ np.testing.assert_array_equal(a, b)
39
+ checks["repeated_question_logits_exact"] = True
40
+ full = model.decide(
41
+ state=state,
42
+ questions={"boolean": questions["boolean"]},
43
+ evidence="none",
44
+ execution="full",
45
+ diagnostics=True,
46
+ )
47
+ c = full["answers"]["boolean"]["branches"][0]["letter_logits"]
48
+ checks["cached_full_max_logit_drift"] = float(np.max(np.abs(np.array(a) - c)))
49
+ assert (first["answers"]["boolean"]["noul"] >= 0.5) == (full["answers"]["boolean"]["noul"] >= 0.5)
50
+ reverse = model.decide(
51
+ state=state,
52
+ questions={"entity": {**questions["entity"], "candidates": ["Bob", "Alice"]}},
53
+ evidence="none",
54
+ )
55
+ assert reverse["answers"]["entity"]["candidates"] == first["answers"]["entity"]["candidates"]
56
+ assert list(reverse["answers"]["entity"]["candidates"]) == ["Bob", "Alice"]
57
+ checks["candidate_order_and_cache_isolation"] = True
58
+ evidence = model.decide(state=state, questions={"boolean": questions["boolean"]}, evidence="removal")
59
+ answer = evidence["answers"]["boolean"]
60
+ assert answer["evidence"]
61
+ for span in answer["evidence"]:
62
+ assert document[span["start"] : span["end"]] == span["text"]
63
+ assert answer["evidence_detail"]["verification"] == "fresh_source_reencoding"
64
+ assert answer["evidence_detail"]["calls"] == 2
65
+ checks["evidence_spans_and_fresh_verification"] = True
66
+ exhausted = model.decide(
67
+ state=state, questions={"boolean": questions["boolean"]}, evidence="removal", evidence_max_calls=0
68
+ )
69
+ assert exhausted["answers"]["boolean"]["evidence_status"] == "budget_exhausted"
70
+ assert exhausted["usage"]["evidence_calls"] == 0
71
+ checks["evidence_budget_enforced"] = True
72
+ replay = output.with_suffix(".replay.json")
73
+ state.save(replay)
74
+ try:
75
+ model.decide(state=state, questions={"q": "Fact?"})
76
+ except ValueError:
77
+ checks["closed_state_rejected"] = True
78
+ else:
79
+ raise AssertionError("Closed state accepted")
80
+ with model.replay(replay) as restored:
81
+ result = model.decide(
82
+ state=restored, questions={"boolean": questions["boolean"]}, evidence="none", diagnostics=True
83
+ )
84
+ np.testing.assert_array_equal(a, result["answers"]["boolean"]["branches"][0]["letter_logits"])
85
+ checks["public_api_replay_exact"] = True
86
+ corrupt = json.loads(replay.read_text())
87
+ corrupt["parts"][0]["text"] += " changed"
88
+ bad_path = output.with_suffix(".corrupt-replay.json")
89
+ bad_path.write_text(json.dumps(corrupt))
90
+ try:
91
+ model.replay(bad_path)
92
+ except ValueError:
93
+ checks["corrupt_replay_rejected"] = True
94
+ else:
95
+ raise AssertionError("Corrupt replay accepted")
96
+ image_parts = json.loads(Path("evaluations/image-jobs.json").read_text())[0]["parts"]
97
+ with model.prefill(image_parts) as images:
98
+ result = model.decide(state=images, questions={"q": "Is Alice certified?"}, evidence="support")
99
+ assert result["answers"]["q"]["evidence_status"] == "unsupported_page_selector"
100
+ checks["missing_page_selector_reported"] = True
101
+ with model.prefill({"subject": "Alice", "certified": True}) as structured:
102
+ assert structured.prefix_tokens > 0
103
+ checks["structured_document_accepted"] = True
104
+ try:
105
+ model.engine.admit(40961)
106
+ except ValueError:
107
+ checks["context_ceiling_enforced"] = True
108
+ else:
109
+ raise AssertionError("Context limit not enforced")
110
+ assert model.engine.context["start"] is None
111
+ assert len(model.engine.heads) == 10
112
+ checks["adapter_state_reset_and_ten_heads_loaded"] = True
113
+ report = {
114
+ "runtime": model.identity,
115
+ "checks": checks,
116
+ "passed": True,
117
+ "scope": "real-weight API behavior; these checks do not establish held-out CUDA parity",
118
+ "answers": first["answers"],
119
+ "evidence": answer,
120
+ }
121
+ output.write_text(json.dumps(report, indent=2))
122
+ print(json.dumps({"passed": True, "checks": checks}, indent=2))
123
+
124
+
125
+ if __name__ == "__main__":
126
+ parser = argparse.ArgumentParser()
127
+ parser.add_argument("--output", required=True)
128
+ validate(parser.parse_args().output)
mlx/src/solomon_mlx/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Solomon semantic decisions on Apple Silicon."""
2
+
3
+ from .api import DocumentState, Solomon
4
+
5
+ __all__ = ["DocumentState", "Solomon"]
mlx/src/solomon_mlx/_vendor/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
mlx/src/solomon_mlx/_vendor/contract.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ import json
4
+ import numpy as np
5
+ MAX_CANDIDATES = 64
6
+
7
+ def _text(value, name):
8
+ if not isinstance(value, str) or not value.strip():
9
+ raise ValueError(name + ' must be a nonempty string')
10
+ return value
11
+
12
+ def parse_questions(questions):
13
+ """Questions {id: spec} -> ordered list of normalised specs.
14
+
15
+ noul: {"type": "noul", "instructions": str} -> task boolean
16
+ {"type": "noul", "instructions": "Is {candidate} ...?", "candidates": [...]} -> task entity (one Noul per candidate)
17
+ {"type": "noul", "instructions": str, "candidates": [...], "candidate_kind": "label"} -> task multilabel
18
+ (candidate_kind defaults to 'entity' when instructions contain {candidate}, else 'label')
19
+ choice: {"type": "choice", "instructions": str, "options": [str, ...] | {key: text}, "ordered": bool}
20
+ ('criteria' is accepted as a compatibility alias of 'options')
21
+ score: {"type": "score", "instructions": str, "levels": [str, ...]} (alias 'criteria'); an ordered choice keyed
22
+ "0".."K-1" with a legend and score = sum_i i * p_i.
23
+ """
24
+ if not isinstance(questions, dict) or not questions:
25
+ raise ValueError('questions must be a nonempty mapping of id -> question')
26
+ out = []
27
+ for qid, spec in questions.items():
28
+ if not isinstance(qid, str) or not qid:
29
+ raise ValueError('question ids must be nonempty strings')
30
+ if isinstance(spec, str):
31
+ spec = {'type': 'noul', 'instructions': spec}
32
+ if not isinstance(spec, dict):
33
+ raise ValueError(f'question {qid}: spec must be an object')
34
+ kind = str(spec.get('type', 'noul')).lower()
35
+ instructions = _text(spec.get('instructions', spec.get('question')), f'question {qid}: instructions')
36
+ if kind == 'noul':
37
+ if 'candidates' not in spec:
38
+ out.append({'id': qid, 'type': 'noul', 'task': 'boolean', 'request': {'question': instructions}})
39
+ continue
40
+ candidates = spec['candidates']
41
+ if not isinstance(candidates, list) or not 1 <= len(candidates) <= MAX_CANDIDATES or len(set(candidates)) != len(candidates) or any((not isinstance(c, str) or not c.strip() for c in candidates)):
42
+ raise ValueError(f'question {qid}: candidates must be 1 to {MAX_CANDIDATES} distinct nonempty strings')
43
+ ckind = spec.get('candidate_kind', 'entity' if '{candidate}' in instructions else 'label')
44
+ if ckind == 'entity':
45
+ if instructions.count('{candidate}') != 1 or '{entity}' in instructions:
46
+ raise ValueError(f'question {qid}: entity instructions need exactly one {{candidate}} placeholder')
47
+ request = {'template': instructions.replace('{candidate}', '{entity}'), 'entities': list(candidates)}
48
+ task = 'entity'
49
+ elif ckind == 'label':
50
+ if '{candidate}' in instructions:
51
+ raise ValueError(f'question {qid}: label candidates take no {{candidate}} placeholder')
52
+ request = {'question': instructions, 'labels': list(candidates)}
53
+ task = 'multilabel'
54
+ else:
55
+ raise ValueError(f'question {qid}: candidate_kind must be entity or label')
56
+ out.append({'id': qid, 'type': 'noul', 'task': task, 'candidates': list(candidates), 'request': request})
57
+ elif kind in ('choice', 'score'):
58
+ raw = spec.get('levels', spec.get('options', spec.get('criteria'))) if kind == 'score' else spec.get('options', spec.get('criteria'))
59
+ if isinstance(raw, dict):
60
+ keys, texts = ([str(k) for k in raw], [raw[k] if isinstance(raw[k], str) and raw[k].strip() else str(k) for k in raw])
61
+ elif isinstance(raw, list):
62
+ texts = list(raw)
63
+ keys = [str(i) for i in range(len(raw))] if kind == 'score' else list(raw)
64
+ else:
65
+ raise ValueError(f'question {qid}: options must be a list or a mapping')
66
+ if not 2 <= len(texts) <= 8 or any((not isinstance(t, str) or not t.strip() for t in texts)) or len(set(texts)) != len(texts):
67
+ raise ValueError(f'question {qid}: provide 2 to 8 distinct nonempty options')
68
+ ordered = kind == 'score' or bool(spec.get('ordered', False))
69
+ if not isinstance(spec.get('ordered', False), bool):
70
+ raise ValueError(f'question {qid}: ordered must be Boolean')
71
+ out.append({'id': qid, 'type': kind, 'task': 'ordered' if ordered else 'single', 'keys': keys, 'texts': texts, 'request': {'question': instructions, 'options': texts}})
72
+ else:
73
+ raise ValueError(f'question {qid}: type must be noul, choice or score')
74
+ return out
75
+
76
+ def _state_parts(state):
77
+ if isinstance(state, str):
78
+ return [{'text': state}]
79
+ if isinstance(state, list):
80
+ return state
81
+ if isinstance(state, dict):
82
+ return [{'text': json.dumps(state, ensure_ascii=False, indent=2, sort_keys=False)}]
83
+ raise ValueError('state must be text, an object, or a list of document parts')
84
+ def empty(spec):
85
+ if spec['type'] == 'noul':
86
+ return {'candidates': None} if 'candidates' in spec else {'noul': None}
87
+ return {'probabilities': None, 'answer': None}
88
+
89
+ def present(spec, dists):
90
+ if spec['type'] == 'noul':
91
+ if 'candidates' in spec:
92
+ return {'candidate_kind': 'entity' if spec['task'] == 'entity' else 'label', 'candidates': {c: float(d[0]) for c, d in zip(spec['candidates'], dists)}, 'candidate_ordering_scores': {c: float(max(d[0], 1 - d[0])) for c, d in zip(spec['candidates'], dists)}}
93
+ return {'noul': float(dists[0][0])}
94
+ p = dists[0]
95
+ keys = spec['keys']
96
+ out = {'probabilities': {k: float(v) for k, v in zip(keys, p)}, 'answer': keys[int(np.argmax(p))]}
97
+ out['choice'] = out['answer']
98
+ if spec['type'] == 'score':
99
+ out['score'] = float(np.dot(np.arange(len(p)), p))
100
+ out['legend'] = dict(zip(keys, spec['texts']))
101
+ elif spec['task'] == 'ordered':
102
+ out['ordered'] = True
103
+ return out
104
+
105
+ def decision(spec, shown):
106
+ if spec['type'] == 'noul':
107
+ if 'candidates' in spec:
108
+ return {c: p >= 0.5 for c, p in shown['candidates'].items()}
109
+ return shown['noul'] >= 0.5
110
+ return shown['answer']
111
+
mlx/src/solomon_mlx/_vendor/evidence.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ """Deterministic source references and explicit evidence interventions.
4
+
5
+ Offsets count Python Unicode code points, never bytes. Candidate ranking is lexical
6
+ and is labelled as such; only an independent model callback supplies support scores.
7
+ Interventions re-encode edited source; an existing KV state cannot prove removal.
8
+ """
9
+ import hashlib
10
+ import re
11
+ from pathlib import Path
12
+
13
+
14
+ def digest(text):
15
+ return hashlib.sha256(text.encode()).hexdigest()
16
+
17
+
18
+ def passages(text, max_chars=1200):
19
+ if not isinstance(text,str) or max_chars < 1:
20
+ raise ValueError('text and positive passage size required')
21
+ result=[]
22
+ # Cover every character, including whitespace; splitting does not normalize text.
23
+ start=0
24
+ while start < len(text):
25
+ limit=min(len(text),start+max_chars)
26
+ end=limit
27
+ if limit < len(text):
28
+ candidates=[m.end() for m in re.finditer(r'\n\s*\n|(?<=[.!?])\s+',text[start:limit])]
29
+ if candidates and candidates[-1] >= max_chars//2:end=start+candidates[-1]
30
+ result.append({'id':f'text:{start}:{end}','kind':'text','start':start,'end':end,
31
+ 'text':text[start:end],'source_sha256':digest(text)})
32
+ start=end
33
+ return result
34
+
35
+
36
+ def image_pages(paths):
37
+ return [{'id':f'page:{i+1}','kind':'image','page':i+1,'path':str(p),
38
+ 'source_sha256':hashlib.sha256(Path(p).read_bytes()).hexdigest()} for i,p in enumerate(paths)]
39
+
40
+
41
+ def validate_spans(text, spans):
42
+ ordered=sorted(spans,key=lambda s:(s['start'],s['end']))
43
+ last=0
44
+ for span in ordered:
45
+ start,end=span['start'],span['end']
46
+ if type(start) is not int or type(end) is not int or not 0<=start<end<=len(text) or start<last:
47
+ raise ValueError('invalid or overlapping evidence range')
48
+ if span.get('source_sha256',digest(text))!=digest(text) or span.get('text',text[start:end])!=text[start:end]:
49
+ raise ValueError('evidence no longer matches source')
50
+ last=end
51
+ return ordered
52
+
53
+
54
+ def rank_candidates(question, candidates, limit=8):
55
+ if limit<1:raise ValueError('positive candidate limit required')
56
+ tokens=set(re.findall(r'\w+',question.casefold()))
57
+ ranked=[]
58
+ for c in candidates:
59
+ words=set(re.findall(r'\w+',c.get('text','').casefold()))
60
+ ranked.append({**c,'candidate_score':len(words&tokens)/max(1,len(tokens)),
61
+ 'candidate_method':'lexical_overlap' if c['kind']=='text' else 'page_order'})
62
+ # Stable ties retain source order (page 2 must precede page 10).
63
+ return sorted(ranked,key=lambda c:-c['candidate_score'])[:limit]
64
+
65
+
66
+ def select(question, candidates, support_score, limit=8, minimum_support=.5):
67
+ """Callback receives only question+candidate, never benchmark gold."""
68
+ if not 0<=minimum_support<=1:raise ValueError('invalid support threshold')
69
+ results=[]
70
+ for candidate in rank_candidates(question,candidates,limit):
71
+ score=float(support_score(question,candidate))
72
+ if not 0<=score<=1:raise ValueError('finite support probability required')
73
+ results.append({**candidate,'support_score':score})
74
+ chosen=[r for r in results if r['support_score']>=minimum_support]
75
+ return {'evidence':chosen,'candidates':results,'verification':'model_support_only',
76
+ 'faithfulness_established':False,'no_support_found':not chosen}
77
+
78
+
79
+ def intervene(text, spans, question, decide):
80
+ """decide(document,question) must prefill each supplied document afresh.
81
+
82
+ All three actual calls are returned; agreement/disagreement is evidence, not a
83
+ guarantee that a passage is the unique cause of an answer.
84
+ """
85
+ spans=validate_spans(text,spans)
86
+ evidence='\n\n'.join(text[s['start']:s['end']] for s in spans)
87
+ pieces=[];start=0
88
+ for span in spans:
89
+ pieces.append(text[start:span['start']]);start=span['end']
90
+ pieces.append(text[start:]);removed=''.join(pieces)
91
+ full=decide(text,question)
92
+ only=decide(evidence,question)
93
+ removal=decide(removed,question)
94
+ return {'full':full,'evidence_only':only,'evidence_removed':removal,
95
+ 'evidence_only_agrees':only==full,'removal_changes_answer':removal!=full,
96
+ 'verification':'fresh_source_reencoding','source_sha256':digest(text),
97
+ 'evidence_sha256':digest(evidence),'removed_sha256':digest(removed),
98
+ 'calls':3,'input_characters':len(text)+len(evidence)+len(removed)}
99
+
100
+
101
+ def validate_pages(pages, selected):
102
+ """Validate ordered page manifests and selected immutable references."""
103
+ indexed = {}
104
+ for expected, page in enumerate(pages, 1):
105
+ if page.get('kind') != 'image' or type(page.get('page')) is not int or page['page'] != expected:
106
+ raise ValueError('image manifest must use consecutive source page IDs')
107
+ if page.get('id') != f'page:{expected}':
108
+ raise ValueError('image page ID does not match source position')
109
+ actual = hashlib.sha256(Path(page['path']).read_bytes()).hexdigest()
110
+ if page.get('source_sha256') != actual:
111
+ raise ValueError('image no longer matches source')
112
+ indexed[expected] = page
113
+ ids = []
114
+ for reference in selected:
115
+ number = reference.get('page')
116
+ if type(number) is not int or number not in indexed or number in ids:
117
+ raise ValueError('invalid or duplicate evidence page')
118
+ source = indexed[number]
119
+ if any(reference.get(key) != source[key] for key in ('id', 'kind', 'path', 'source_sha256')):
120
+ raise ValueError('evidence page no longer matches source manifest')
121
+ ids.append(number)
122
+ return [indexed[number] for number in sorted(ids)]
123
+
124
+
125
+ def intervene_pages(pages, selected, question, decide, text=''):
126
+ """Re-encode full, evidence-only and page-removed multimodal documents.
127
+
128
+ decide(text, image_paths, question) must create a fresh state each time and
129
+ accept an empty image list. Evidence-only has no accompanying source text;
130
+ removal retains all source text and unselected pages. This isolates page
131
+ evidence and makes text-only sufficiency a visible competing explanation.
132
+ Original page IDs are recorded because subset images are renumbered on input.
133
+ """
134
+ if not isinstance(text, str):
135
+ raise ValueError('source text must be a string')
136
+ selected = validate_pages(pages, selected)
137
+ selected_ids = {p['page'] for p in selected}
138
+ removed = [p for p in pages if p['page'] not in selected_ids]
139
+ calls = [(text, pages), ('', selected), (text, removed)]
140
+ answers = []
141
+ for source_text, source_pages in calls:
142
+ # Recheck all originals before each call: never silently mix revisions.
143
+ validate_pages(pages, selected)
144
+ answers.append(decide(source_text, [p['path'] for p in source_pages], question))
145
+ validate_pages(pages, selected)
146
+ full, only, removal = answers
147
+ return {'full': full, 'evidence_only': only, 'evidence_removed': removal,
148
+ 'evidence_only_agrees': only == full, 'removal_changes_answer': removal != full,
149
+ 'verification': 'fresh_source_reencoding', 'faithfulness_established': False,
150
+ 'source_text_sha256': digest(text),
151
+ 'source_pages': [{k: p[k] for k in ('id', 'page', 'source_sha256')} for p in pages],
152
+ 'evidence_page_ids': [p['page'] for p in selected],
153
+ 'removed_input_page_ids': [p['page'] for p in removed],
154
+ 'calls': 3, 'input_images': sum(len(p) for _, p in calls),
155
+ 'input_image_bytes': sum(Path(p['path']).stat().st_size for _, ps in calls for p in ps),
156
+ 'input_characters': 2 * len(text)}
mlx/src/solomon_mlx/_vendor/evidence_v3.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ """Evidence packages (v3): selected spans plus source-derived governing context, per question unit.
4
+
5
+ A package is a genuine subset of one source document: every range is an exact, validated slice
6
+ of the source (Python code-point offsets), rendered in source order with whitespace-only
7
+ separators. Nothing is paraphrased, summarised or generated. Context is attached by
8
+ question-independent document structure plus the unit's own selected spans and question text;
9
+ gold/authoring data is never an input.
10
+
11
+ Roles
12
+ evidence spans chosen by the selector (or, in oracle diagnostics, gold anchors). Span
13
+ precision/recall gates are computed on these only, exactly as before.
14
+ context ranges attached automatically, each with one or more reasons:
15
+ header document title block (identity of the source, author/compiler, date)
16
+ interpretation generic reading rules (scope6.retrieval.governing_context, unchanged regex)
17
+ locator container of an included span: message header line (date, author ->
18
+ recipient), minute number + heading, schedule/section heading, entry number
19
+ correction a withdrawal/rescission/deletion elsewhere that refers to the container of an
20
+ included statement and names the same subject and topic (or the exact entry)
21
+ withdrawn the statement(s) that an included withdrawal refers to, so the chain is
22
+ readable (the withdrawn text is marked by the withdrawal, not by us)
23
+ rule a general rule / band scale whose operative topic matches the unit question
24
+ (attached only when the unit has evidence)
25
+ exception the exception clause governing an included rule, or the rule an included
26
+ exception limits
27
+ definition the sentence defining a capitalised class term used by an included rule,
28
+ when it names the unit's subject
29
+ condition a statement about the unit's subject that bears on an included rule's
30
+ conditions or exception (>=2 shared content words beyond the question topic,
31
+ or one document-rare shared word)
32
+ convention the document's own reading conventions (sentences of the paragraphs that hold
33
+ generic reading rules) that the package needs: silence always; conflict and vocabulary when
34
+ there is evidence; removal when a withdrawal/deletion chain is present;
35
+ condition when a rule is present; exception when an exception is present
36
+
37
+ Size is measured as covered source characters / source characters and reported per package. A
38
+ package above the registered cap (default 0.30) sheds context in TRIM_ORDER (never evidence, rule,
39
+ exception, condition, correction or withdrawn context) and records what was trimmed.
40
+ """
41
+ import re
42
+ from .evidence import digest, validate_spans
43
+ from .retrieval import candidates, governing_context
44
+
45
+ SCHEMA = 'scope6-evidence-package-v3.1'
46
+ SIZE_CAP = .30
47
+ # Context dropped first when a package exceeds the size cap (evidence is never dropped).
48
+ TRIM_ORDER = ('interpretation', 'convention/silence', 'convention/vocabulary', 'convention/conflict', 'header', 'definition',
49
+ 'convention/condition', 'convention/exception', 'convention/removal', 'locator')
50
+ MONTHS = 'January|February|March|April|May|June|July|August|September|October|November|December'
51
+ _WORD = re.compile(r'\w+')
52
+ _CAP = re.compile(r"\b[A-Z][\w&'-]*")
53
+ _WITHDRAWAL = re.compile(r"withdr[ae]w|withdrawn|take back|disregard|rescind|retract|should not be relied|"
54
+ r"substituted|\bis deleted|\bdelete[sd]?\b|struck out|replaced by|no further effect", re.I)
55
+ _REF_DATE = re.compile(r'(?:message|wrote|written|letter|note)\D{0,14}?(\d{1,2} (?:' + MONTHS + r'))')
56
+ _REF_MINUTE = re.compile(r'\bminute (\d+)\b', re.I)
57
+ _REF_ENTRY = re.compile(r'\b[Ee]ntry (\d+) of [Ss]chedule (\d+)')
58
+ _MSG_HEAD = re.compile(r'Message \d+\. (\d{1,2} (?:' + MONTHS + r'))\.')
59
+ _MINUTE_HEAD = re.compile(r'(\d+)\.\s+[^.\n]{1,80}\.')
60
+ _SCHEDULE_HEAD = re.compile(r'SCHEDULE (\d+)\b')
61
+ _ENTRY_PREFIX = re.compile(r'[A-Z]?\d+(?:\.\d+)*\.?\s*')
62
+ _PERMISSION = re.compile(r'\bmay\b|permitted|authorised|entitled|leave to|allowed|cleared|confers?', re.I)
63
+ _RULE = re.compile(r"(?i:\b(?:any|each|every)\s+[a-z]+(?:\s+[a-z]+)?\s+(?:who|that)\b|\ban?\s+[a-z]+\s+who\b|general rule|standing rule|"
64
+ r"office rule|rule governing|\bif that\b|where one and the same|\b[a-z]+s who\b|\bwhere (?:an?|any|one)\b)|\b[A-Z][a-z]+ [A-Z][a-z]+s? (?:that|who)\b")
65
+ _SCALE = re.compile(r'\bbands?\b|\bscale\b|from lowest to highest|order of the levels|worked out from|fixed by that number|\bfewer than \d+', re.I)
66
+ _EXCEPTION = re.compile(r'does not apply|confers nothing on|is outside (?:clause|the rule)|not engaged|does not reach|switched off|'
67
+ r'disapplied|subject to (?:one|the) exception|nothing in the rule|save that|except (?:where|that|for)\b', re.I)
68
+ CONVENTIONS = {
69
+ 'removal': re.compile(r"withdr[ae]w|rescind|delet|substitut|struck out|take back|displace|disregard|\bspent\b|express(?:ly)? (?:withdrawal|rescission)", re.I),
70
+ 'condition': re.compile(r"\bconditions?\b|\blimbs?\b|requirements?\b|only part|part of the way|whole of it|each of its|all of its", re.I),
71
+ 'exception': re.compile(r"\bexcept", re.I),
72
+ 'conflict': re.compile(r"both (?:stand|remain|hold|are in force|left standing)|opposite (?:ways|things)|inconsistent|contradict|"
73
+ r"point opposite|not say which|not chosen between|this office does not say", re.I),
74
+ 'vocabulary': re.compile(r"interchangeabl|one and the same (?:permission|refusal)|are one (?:grant|refusal)|mean the same|same thing|words to like effect", re.I),
75
+ 'silence': re.compile(r"silen|absence of|unminuted|nothing has been decided|has not been said|undecided|left open", re.I)}
76
+ _DEFINES = re.compile(r'\bare\b|\bmeans\b|\bidentified\b|\binclude', re.I)
77
+ _STOP = {'this', 'that', 'these', 'those', 'with', 'under', 'which', 'what', 'does', 'file', 'correspondence', 'record', 'records',
78
+ 'recorded', 'minutes', 'agreement', 'bundle', 'messages', 'message', 'stand', 'stands', 'taking', 'reading', 'whole',
79
+ 'strength', 'position', 'open', 'given', 'have', 'been', 'applies', 'apply', 'entitled', 'allowed', 'liberty', 'free',
80
+ 'from', 'there', 'their', 'they', 'decisions', 'here', 'schedules', 'schedule', 'papers', 'office', 'shown', 'show',
81
+ 'shows', 'case', 'matters', 'terms', 'place', 'placed', 'about', 'question', 'whether', 'should', 'relied', 'either',
82
+ 'wrote', 'withdraw', 'withdrawn', 'passage', 'treat', 'nothing', 'follows', 'please', 'disregard', 'concerns', 'deals',
83
+ 'resolved', 'rescinded', 'much', 'decision', 'minute', 'further', 'effect', 'permission', 'permitted', 'refused',
84
+ 'barred', 'prohibited', 'authorised', 'cleared', 'leave', 'allows', 'allow', 'bars', 'refuses', 'grant', 'refusal',
85
+ 'entry', 'deleted', 'substituted', 'following', 'replaced', 'struck', 'committee', 'secretary', 'chair', 'principal'}
86
+ ROLES = ('evidence', 'context')
87
+ # Capitalised words that are never subject names (sentence openers, document furniture).
88
+ _NOT_NAMES = {'the', 'this', 'that', 'these', 'those', 'it', 'its', 'i', 'we', 'our', 'my', 'on', 'for', 'so', 'as', 'at', 'in', 'of',
89
+ 'please', 'treat', 'where', 'when', 'what', 'which', 'who', 'whether', 'any', 'each', 'every', 'no', 'nothing', 'there',
90
+ 'message', 'messages', 'entry', 'schedule', 'clause', 'minute', 'minutes', 'agreement', 'principal', 'resolved', 'note',
91
+ 'committee', 'secretary', 'chair', 'treasurer', 'register', 'records', 'record', 'permission', 'an', 'a', 'if', 'all',
92
+ 'both', 'neither', 'either', 'one', 'two', 'words', 'dates', 'decisions', 'rules', 'only', 'once', 'part', 'to', 'by',
93
+ 'read', 'do', 'what', 'should', 'with', 'from', 'under', 'after', 'before', 'reading', 'taking', 'is', 'has', 'have'}
94
+
95
+
96
+ def _words(s):
97
+ return set(_WORD.findall(s.casefold()))
98
+
99
+
100
+ def _topic(s, names=()):
101
+ low = {w for n in names for w in _words(n)}
102
+ return {w for w in _words(s) if len(w) >= 4 and w not in _STOP and w not in low and not w.isdigit()
103
+ and not re.fullmatch(MONTHS.casefold(), w)}
104
+
105
+
106
+ class Structure:
107
+ """Question-independent source structure: sentences, paragraphs, lines and containers."""
108
+
109
+ def __init__(self, text):
110
+ self.text = text
111
+ self.sentences = candidates(text)
112
+ self.paragraphs = []
113
+ start = 0
114
+ for m in list(re.finditer(r'\n\s*\n', text)) + [None]:
115
+ end = m.start() if m else len(text)
116
+ if text[start:end].strip():
117
+ self.paragraphs.append((start, end))
118
+ start = m.end() if m else len(text)
119
+ self.lines = [(m.start(), m.end()) for m in re.finditer(r'[^\n]+', text)]
120
+ df = {}
121
+ for s in self.sentences:
122
+ for w in _words(s['text']):
123
+ df[w] = df.get(w, 0) + 1
124
+ self.df = df
125
+ self.interpretation = [(s['start'], s['end']) for s in governing_context(text, self.sentences)]
126
+ # containers: message date -> paragraph; minute number -> paragraph; (schedule, entry) -> line
127
+ self.messages, self.minutes, self.entries = {}, {}, {}
128
+ for a, b in self.paragraphs:
129
+ first = text[a:b].split('\n', 1)[0]
130
+ m = _MSG_HEAD.match(first)
131
+ if m:
132
+ self.messages.setdefault(m.group(1), (a, b))
133
+ m = re.match(r'(\d+)\. ', text[a:b])
134
+ if m and '\n' not in text[a:b].strip():
135
+ self.minutes.setdefault(m.group(1), (a, b))
136
+ m = _SCHEDULE_HEAD.match(first)
137
+ if m:
138
+ for la, lb in self.lines:
139
+ if a <= la and lb <= b:
140
+ e = re.match(r'(\d+)\. ', text[la:lb])
141
+ if e:
142
+ self.entries[(m.group(1), e.group(1))] = (la, lb)
143
+ self.rules = [s for s in self.sentences if (_RULE.search(s['text']) and _PERMISSION.search(s['text'])) or _SCALE.search(s['text'])]
144
+ self.exceptions = [s for s in self.sentences if _EXCEPTION.search(s['text'])]
145
+ self.withdrawals = [s for s in self.sentences if _WITHDRAWAL.search(s['text'])]
146
+ # Reading conventions: sentences of the interpretation paragraphs (those holding a generic reading rule).
147
+ blocks = {self.paragraph_of(a) for a, _ in self.interpretation} - {None}
148
+ general = [s for s in self.sentences if self.paragraph_of(s['start']) in blocks and s not in self.rules]
149
+ self.conventions = {k: [(s['start'], s['end']) for s in general if rx.search(s['text'])] for k, rx in CONVENTIONS.items()}
150
+
151
+ def governed_rule(self, exception):
152
+ """The rule an exception limits: nearest preceding rule in the same paragraph (or the same sentence)."""
153
+ para = self.paragraph_of(exception['start'])
154
+ prior = [r for r in self.rules if r['start'] <= exception['start'] and para and para[0] <= r['start'] < para[1]
155
+ and not _SCALE.search(r['text'])]
156
+ return prior[-1] if prior else None
157
+
158
+ def names(self, s, limit=16):
159
+ """Capitalised tokens that are rare in this source (subject names); months/number words excluded."""
160
+ out = set()
161
+ for tok in _CAP.findall(s):
162
+ tok = re.sub(r"'s$", '', tok)
163
+ w = tok.casefold()
164
+ if re.fullmatch(MONTHS, tok) or len(w) < 3 or w in _NOT_NAMES or (tok.isupper() and len(tok) > 1):
165
+ continue
166
+ if self.df.get(w, 0) <= limit:
167
+ out.add(tok)
168
+ return out
169
+
170
+ def paragraph_of(self, pos):
171
+ for a, b in self.paragraphs:
172
+ if a <= pos < b:
173
+ return a, b
174
+ return None
175
+
176
+ def line_of(self, pos):
177
+ for a, b in self.lines:
178
+ if a <= pos < b:
179
+ return a, b
180
+ return None
181
+
182
+ def container(self, span):
183
+ """(kind, key) of the message/minute/entry holding a span, else None."""
184
+ for key, (a, b) in self.entries.items():
185
+ if a <= span['start'] < b:
186
+ return ('entry', key)
187
+ para = self.paragraph_of(span['start'])
188
+ if para is None:
189
+ return None
190
+ for key, rng in self.messages.items():
191
+ if rng == para:
192
+ return ('message', key)
193
+ for key, rng in self.minutes.items():
194
+ if rng == para:
195
+ return ('minute', key)
196
+ return None
197
+
198
+ def references(self, sentence):
199
+ """Containers a withdrawal-type sentence refers to."""
200
+ out = []
201
+ for m in _REF_ENTRY.finditer(sentence):
202
+ out.append(('entry', (m.group(2), m.group(1))))
203
+ for m in _REF_DATE.finditer(sentence):
204
+ out.append(('message', m.group(1)))
205
+ for m in _REF_MINUTE.finditer(sentence):
206
+ out.append(('minute', m.group(1)))
207
+ return out
208
+
209
+ def container_range(self, ref):
210
+ kind, key = ref
211
+ return {'entry': self.entries, 'message': self.messages, 'minute': self.minutes}[kind].get(key)
212
+
213
+ def locators(self, span):
214
+ """Header ranges that place a span in its container (never the span's own text)."""
215
+ text, out = self.text, []
216
+ para = self.paragraph_of(span['start'])
217
+ if para is None:
218
+ return out
219
+ a, b = para
220
+ nl = text.find('\n', a, b)
221
+ if nl >= 0 and span['start'] > nl:
222
+ out.append((a, nl)) # first line of a multi-line block: message/section/schedule heading
223
+ else:
224
+ m = _MINUTE_HEAD.match(text, a)
225
+ if m and m.end() <= span['start']:
226
+ out.append((a, m.end())) # numbered minute and its heading
227
+ line = self.line_of(span['start'])
228
+ if line and line[0] < span['start']:
229
+ prefix = text[line[0]:span['start']]
230
+ if len(prefix) <= 16 and _ENTRY_PREFIX.fullmatch(prefix):
231
+ out.append((line[0], span['start'])) # entry / clause number
232
+ return out
233
+
234
+ def sentence_ranges_in(self, rng):
235
+ a, b = rng
236
+ return [s for s in self.sentences if a <= s['start'] < b]
237
+
238
+
239
+ def _strip(text, a, b):
240
+ while a < b and text[a].isspace():
241
+ a += 1
242
+ while b > a and text[b - 1].isspace():
243
+ b -= 1
244
+ return a, b
245
+
246
+
247
+ def _merge(text, items):
248
+ """items: (start, end, role, reason) -> merged validated ranges; evidence role wins on overlap."""
249
+ cleaned = []
250
+ for a, b, role, reason in items:
251
+ a, b = _strip(text, a, b)
252
+ if a < b:
253
+ cleaned.append((a, b, role, reason))
254
+ cleaned.sort()
255
+ merged = []
256
+ for a, b, role, reason in cleaned:
257
+ if merged and a <= merged[-1]['end']:
258
+ m = merged[-1]
259
+ m['end'] = max(m['end'], b)
260
+ m['roles'].add(role)
261
+ m['reasons'].add(reason)
262
+ else:
263
+ merged.append({'start': a, 'end': b, 'roles': {role}, 'reasons': {reason}})
264
+ h = digest(text)
265
+ out = [{'id': f"text:{m['start']}:{m['end']}", 'kind': 'text', 'start': m['start'], 'end': m['end'],
266
+ 'text': text[m['start']:m['end']], 'source_sha256': h,
267
+ 'role': 'evidence' if 'evidence' in m['roles'] else 'context', 'reasons': sorted(m['reasons'])} for m in merged]
268
+ validate_spans(text, out)
269
+ return out
270
+
271
+
272
+ def render(text, spans):
273
+ """Source-order rendering; separators are whitespace only and mirror the source layout."""
274
+ parts, last = [], None
275
+ for s in sorted(spans, key=lambda s: s['start']):
276
+ if last is not None:
277
+ gap = text[last:s['start']]
278
+ parts.append('\n\n' if '\n\n' in gap or re.search(r'\n\s*\n', gap) else ('\n' if '\n' in gap else ' '))
279
+ parts.append(text[s['start']:s['end']])
280
+ last = s['end']
281
+ return ''.join(parts)
282
+
283
+
284
+ def build(text, question, evidence, *, subject=None, structure=None, options=None, cap=SIZE_CAP):
285
+ """One unit's evidence package.
286
+
287
+ evidence: selected (or gold, for oracle diagnostics) source spans for this unit.
288
+ subject: optional explicit subject string (entity name); otherwise rare capitalised question tokens.
289
+ """
290
+ opts = {'header': True, 'interpretation': True, 'locator': True, 'correction': True, 'withdrawn': True,
291
+ 'rule': True, 'exception': True, 'definition': True, 'condition': True, 'convention': True, **(options or {})}
292
+ st = structure or Structure(text)
293
+ evidence = validate_spans(text, [{k: s[k] for k in ('start', 'end')} for s in evidence]) if evidence else []
294
+ items = [(s['start'], s['end'], 'evidence', 'selected') for s in evidence]
295
+ subj = st.names(subject if subject else question, limit=10 ** 9 if subject else 16)
296
+ if subject:
297
+ subj |= {subject}
298
+ qtopic = _topic(question, subj)
299
+ rare = st.names(question)
300
+
301
+ def mentions(t):
302
+ return (subject in t) if subject else bool(st.names(t) & rare)
303
+ if opts['header'] and st.paragraphs:
304
+ a, b = st.paragraphs[0]
305
+ items.append((a, min(b, a + 400), 'context', 'header'))
306
+ if opts['interpretation']:
307
+ items += [(a, b, 'context', 'interpretation') for a, b in st.interpretation]
308
+ if evidence:
309
+ included = [dict(s) for s in evidence]
310
+ if opts['rule']:
311
+ for r in st.rules:
312
+ if len(_topic(r['text'], subj) & qtopic) >= (3 if 'Attribute:' in question else 2):
313
+ items.append((r['start'], r['end'], 'context', 'rule'))
314
+ included.append(r)
315
+ # Iterate twice: attached statements can themselves need locators/corrections.
316
+ for _ in range(3):
317
+ current = [{'start': a, 'end': b} for a, b, role, reason in items
318
+ if role == 'evidence' or reason in ('rule', 'exception', 'withdrawn', 'correction', 'definition', 'condition')]
319
+ spans = [s for s in st.sentences if any(max(s['start'], c['start']) < min(s['end'], c['end']) for c in current)]
320
+ for s in spans:
321
+ s_text = s['text']
322
+ if opts['exception'] and s in st.rules:
323
+ for e in st.exceptions:
324
+ g = st.governed_rule(e)
325
+ if g is not None and g['start'] == s['start']:
326
+ items.append((e['start'], e['end'], 'context', 'exception'))
327
+ if opts['exception'] and s in st.exceptions:
328
+ g = st.governed_rule(s)
329
+ if g is not None:
330
+ items.append((g['start'], g['end'], 'context', 'exception'))
331
+ if opts['definition'] and s in st.rules:
332
+ for term in set(re.findall(r'(?<=[a-z,;] )([A-Z][a-z]+ [A-Z][a-z]+?)s?\b', s_text)):
333
+ for d in st.sentences:
334
+ if term in d['text'] and _DEFINES.search(d['text']) and d['start'] < s['start'] and mentions(d['text']):
335
+ items.append((d['start'], d['end'], 'context', 'definition'))
336
+ if opts['condition'] and s in st.rules:
337
+ # Facts about this unit's subject that bear on the rule's conditions or its exception.
338
+ governing = [s] + [e for e in st.exceptions if (st.governed_rule(e) or {}).get('start') == s['start']]
339
+ words = set().union(*(_topic(g['text'], subj) for g in governing)) - qtopic
340
+ for t in st.sentences:
341
+ shared = _topic(t['text'], subj) & words
342
+ if t['start'] != s['start'] and mentions(t['text']) and (len(shared) >= 2 or any(st.df.get(w, 0) <= 3 for w in shared)):
343
+ items.append((t['start'], t['end'], 'context', 'condition'))
344
+ if opts['locator']:
345
+ items += [(a, b, 'context', 'locator') for a, b in st.locators(s)]
346
+ is_withdrawal = bool(_WITHDRAWAL.search(s_text))
347
+ if opts['withdrawn'] and is_withdrawal:
348
+ wnames = st.names(s_text)
349
+ for ref in st.references(s_text):
350
+ rng = st.container_range(ref)
351
+ if rng is None:
352
+ continue
353
+ if ref[0] == 'entry':
354
+ items.append((rng[0], rng[1], 'context', 'withdrawn'))
355
+ continue
356
+ for t in st.sentence_ranges_in(rng):
357
+ if t['start'] == s['start']:
358
+ continue
359
+ if st.names(t['text']) & wnames and _topic(t['text'], wnames) & _topic(s_text, wnames):
360
+ items.append((t['start'], t['end'], 'context', 'withdrawn'))
361
+ if opts['correction'] and not is_withdrawal:
362
+ where = st.container(s)
363
+ if where is None:
364
+ continue
365
+ snames = st.names(s_text)
366
+ for w in st.withdrawals:
367
+ if w['start'] == s['start'] or where not in st.references(w['text']):
368
+ continue
369
+ if where[0] == 'entry' or (st.names(w['text']) & snames and _topic(w['text'], snames) & _topic(s_text, snames)):
370
+ items.append((w['start'], w['end'], 'context', 'correction'))
371
+ if opts['convention']:
372
+ reasons = {r for _, _, _, r in items}
373
+ ev_text = ' '.join(text[a:b] for a, b, role, _ in items if role == 'evidence')
374
+ wanted = {'silence'}
375
+ if evidence:
376
+ wanted |= {'conflict', 'vocabulary'}
377
+ if reasons & {'correction', 'withdrawn'} or _WITHDRAWAL.search(ev_text):
378
+ wanted.add('removal')
379
+ if 'rule' in reasons or any(r['start'] < b and a < r['end'] for r in st.rules if not _SCALE.search(r['text'])
380
+ for a, b, role, _ in items if role == 'evidence'):
381
+ wanted.add('condition')
382
+ if 'exception' in reasons:
383
+ wanted.add('exception')
384
+ for kind in sorted(wanted):
385
+ items += [(a, b, 'context', 'convention/'+kind) for a, b in st.conventions[kind]]
386
+ spans = _merge(text, items);trimmed = []
387
+ def size(sp):return sum(x['end'] - x['start'] for x in sp) / len(text) if text else 0.
388
+ for reason in (TRIM_ORDER if cap is not None else ()):
389
+ if size(spans) <= cap:
390
+ break
391
+ keep = [it for it in items if it[3] != reason]
392
+ if len(keep) != len(items):
393
+ items = keep;trimmed.append(reason);spans = _merge(text, items)
394
+ rendered = render(text, spans)
395
+ covered = sum(s['end'] - s['start'] for s in spans)
396
+ return {'schema': SCHEMA, 'question': question, 'source_sha256': digest(text), 'text': rendered, 'spans': spans,
397
+ 'size_cap': cap, 'within_size_cap': cap is None or covered <= cap * len(text), 'trimmed_for_cap': trimmed,
398
+ 'evidence_ranges': [{'start': s['start'], 'end': s['end']} for s in evidence],
399
+ 'no_support_found': not evidence, 'source_characters': len(text), 'covered_characters': covered,
400
+ 'rendered_characters': len(rendered), 'source_fraction': covered / len(text) if text else 0.,
401
+ 'context_reasons': sorted({r for s in spans for r in s['reasons'] if r != 'selected'}),
402
+ 'faithfulness_established': False}
403
+
404
+
405
+ def union(text, packages):
406
+ """One document-level package: union of unit packages (used for whole-question rendering)."""
407
+ items = [(s['start'], s['end'], s['role'], r) for p in packages for s in p['spans'] for r in s['reasons']]
408
+ if not items:
409
+ return {'schema': SCHEMA, 'text': '', 'spans': [], 'source_fraction': 0., 'covered_characters': 0,
410
+ 'source_characters': len(text), 'rendered_characters': 0, 'source_sha256': digest(text)}
411
+ spans = _merge(text, items)
412
+ rendered = render(text, spans)
413
+ covered = sum(s['end'] - s['start'] for s in spans)
414
+ return {'schema': SCHEMA, 'text': rendered, 'spans': spans, 'source_fraction': covered / len(text),
415
+ 'covered_characters': covered, 'source_characters': len(text), 'rendered_characters': len(rendered),
416
+ 'source_sha256': digest(text)}
417
+
418
+
419
+ def with_pages(package, text, page_map):
420
+ """Attach renderer page IDs to every package span (scope6.sources.evidence_pages)."""
421
+ from scope6.sources import evidence_pages
422
+ out = dict(package)
423
+ out['spans'] = [{**s, 'pages': evidence_pages(text, [s], page_map)} for s in package['spans']]
424
+ out['pages'] = sorted({p for s in out['spans'] for p in s['pages']})
425
+ return out
mlx/src/solomon_mlx/_vendor/prompts.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ SYSTEM = 'You answer questions about the supplied document. Use only the document. Task instructions follow the document; follow them exactly.'
4
+
5
+ BOOLEAN_TASK = 'Task: classify the evidence for the question using only the document and its explicit rules. A = Yes only. B = No only. C = neither Yes nor No is established. D = both Yes and No are established. A missing fact is not a negative fact. Evidence about another person or subject does not contradict the queried one. Apply explicit time and replacement rules before deciding. Respond with exactly one letter: A, B, C, or D. Do not explain.'
6
+
7
+ CHOICE_TASK = 'Task: choose the single option that the document best supports. Respond with exactly one letter. Do not explain.'
8
+
9
+ def boolean_block(question):
10
+ return BOOLEAN_TASK + '\nQuestion: ' + question + '\nAnswer (one letter):'
11
+ LETTERS = 'ABCDEFGHIJ'
12
+
13
+ PAGE = '<|vision_start|><|image_pad|><|vision_end|>'
14
+
15
+ RESERVED = ['The document does not state this', 'The document gives conflicting answers']
16
+
17
+ SINGLE_R = 'Task: choose the single option that the document establishes as the answer. If the document does not establish any of the other listed answers, choose the option that says it does not state this. If the document establishes two different listed answers and does not say which prevails, choose the option that says it gives conflicting answers. A replaced or withdrawn statement establishes nothing. Respond with exactly one letter. Do not explain.'
18
+
19
+ ORDERED_R = 'Task: the options form an ordered scale, lowest first, followed by two special options. Choose the single level that the document establishes. If the document does not establish any level, choose the option that says it does not state this. If it establishes two different levels and does not say which prevails, choose the option that says it gives conflicting answers. A replaced or withdrawn statement establishes nothing. Respond with exactly one letter. Do not explain.'
20
+
21
+ SINGLE_S = CHOICE_TASK
22
+
23
+ ORDERED_S = 'Task: the options form an ordered scale, lowest first. Choose the single level that the document best supports. Respond with exactly one letter. Do not explain.'
24
+
25
+ SUFFICIENCY = 'Task: classify what the document establishes about the answer to the question, using only the document. A = it establishes exactly one of the listed answers. B = it does not establish any of the listed answers. C = it establishes two or more different listed answers and does not say which prevails. A missing fact is not a negative fact. A replaced or withdrawn statement establishes nothing. Respond with exactly one letter: A, B, or C. Do not explain.'
26
+
27
+ LABEL = 'Task: decide whether the label applies, using only the document. A = the document establishes that it applies, and nothing in it establishes that it does not. B = the document establishes that it does not apply, and nothing in it establishes that it does. C = the document establishes neither. D = the document establishes both. A missing fact is not a negative fact. Evidence about another person or subject does not count. A replaced or withdrawn statement establishes nothing. Respond with exactly one letter: A, B, C, or D. Do not explain.'
28
+
29
+ OPTION = 'Task: decide whether the proposed answer is correct, using only the document. A = the document establishes this answer, and nothing in it establishes a different one. B = the document establishes a different answer, or establishes that this one is wrong, and nothing in it establishes this one. C = the document establishes neither. D = the document establishes both this answer and a different one. A missing fact is not a negative fact. A replaced or withdrawn statement establishes nothing. Respond with exactly one letter: A, B, C, or D. Do not explain.'
30
+
31
+ def _lettered(options):
32
+ return '\n'.join((f'{LETTERS[i]}. {text}' for i, text in enumerate(options)))
33
+
34
+ def listwise_block(question, options, ordered=False, reserved=True):
35
+ """Caller options in caller order; with `reserved` the service appends the two reserved outcomes."""
36
+ if not 2 <= len(options) <= 8 or len(set(options)) != len(options):
37
+ raise ValueError('needs 2 to 8 distinct options')
38
+ shown = list(options) + (RESERVED if reserved else [])
39
+ task = (ORDERED_R if ordered else SINGLE_R) if reserved else ORDERED_S if ordered else SINGLE_S
40
+ return (task + '\nQuestion: ' + question + '\nOptions:\n' + _lettered(shown) + '\nAnswer (one letter):', len(shown))
41
+
42
+ def label_block(question, label):
43
+ return (LABEL + '\nQuestion: ' + question + '\nLabel: ' + label + '\nAnswer (one letter):', 4)
mlx/src/solomon_mlx/_vendor/retrieval.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ """Inference-only source candidates and explicitly labelled retrieval baselines."""
4
+ import re
5
+ from .evidence import digest,rank_candidates,validate_spans
6
+
7
+ def candidates(text):
8
+ """Source sentences retaining exact character ranges; no authoring labels."""
9
+ ends=[m.end() for m in re.finditer(r'(?<=[.!?])(?:[ \t]+|\n+)|\n\s*\n',text)]+[len(text)]
10
+ result=[];start=0
11
+ for end in ends:
12
+ if end>start and text[start:end].strip():
13
+ result.append({'id':f'text:{start}:{end}','kind':'text','start':start,'end':end,
14
+ 'text':text[start:end],'source_sha256':digest(text)})
15
+ start=end
16
+ return result
17
+
18
+ def governing_context(text,spans=None):
19
+ """Generic rule/retraction interpretation context, independent of question gold."""
20
+ spans=candidates(text) if spans is None else spans
21
+ pattern=re.compile(r'general rule|both limbs|same (?:person|courier|member|officer)|neither limb|missing requirement|silence|withdrawn passage|dates and seniority|a withdrawn|outside these|bands are|order of the levels|no.*precedence|each passage|a grant in force|an exception to a rule',re.I)
22
+ return [s for s in spans if pattern.search(s['text'])]
23
+
24
+ def merge(text,spans):
25
+ intervals=[]
26
+ for span in sorted(spans,key=lambda s:(s['start'],s['end'])):
27
+ a,b=span['start'],span['end']
28
+ if intervals and a<=intervals[-1][1]:intervals[-1]=(intervals[-1][0],max(b,intervals[-1][1]))
29
+ else:intervals.append((a,b))
30
+ result=[{'id':f'text:{a}:{b}','kind':'text','start':a,'end':b,'text':text[a:b],'source_sha256':digest(text)} for a,b in intervals]
31
+ return validate_spans(text,result)
32
+
33
+ def render_subset(text,spans,*,include_context=True):
34
+ selected=merge(text,list(spans)+(governing_context(text) if include_context else []))
35
+ return '\n\n'.join(s['text'] for s in selected),selected
36
+
37
+ _WORD=re.compile(r'\w+')
38
+ _SCALE=re.compile(r'\bbands?\b|\bscale\b|from lowest to highest|order of the levels',re.I)
39
+ POOL_V2={'name':'idf-rare-rule-v2','idf_limit':24,'rare_df':16,'rules':True}
40
+
41
+ def _words(s):return set(_WORD.findall(s.casefold()))
42
+
43
+ def rule_candidates(text,spans=None):
44
+ """Question-independent rule, exception, withdrawal-convention and scale passages."""
45
+ spans=candidates(text) if spans is None else spans
46
+ rules={s['id'] for s in governing_context(text,spans)}
47
+ return [s for s in spans if s['id'] in rules or _SCALE.search(s['text'])]
48
+
49
+ def candidate_pool(question,spans,text=None,*,idf_limit=24,rare_df=16,rules=True,**_):
50
+ """Bounded source-only pool: document-IDF top-k, every passage sharing a rare
51
+ question term (typically the subject's name), plus question-independent rules.
52
+
53
+ Candidate order is source order. candidate_score stays the plain lexical-overlap
54
+ fraction used by the frozen relevance-head feature, so heads remain comparable.
55
+ Gold is never an input.
56
+ """
57
+ import math
58
+ if idf_limit<1 or rare_df<0:raise ValueError('invalid candidate pool policy')
59
+ ranked=rank_candidates(question,spans,len(spans)) if spans else []
60
+ words=[_words(c['text']) for c in ranked];q=_words(question);n=len(ranked)
61
+ df={}
62
+ for ws in words:
63
+ for w in ws:df[w]=df.get(w,0)+1
64
+ idf=[sum(math.log((n+1)/(df[w]+.5)) for w in ws&q) for ws in words]
65
+ order=sorted(range(n),key=lambda i:(-idf[i],ranked[i]['start']))
66
+ reason={}
67
+ for i in order[:idf_limit]:reason.setdefault(ranked[i]['id'],'idf')
68
+ rare={w for w in q if df.get(w,0)<=rare_df}
69
+ for c,ws in zip(ranked,words):
70
+ if ws&rare:reason.setdefault(c['id'],'rare_term')
71
+ if rules:
72
+ for c in rule_candidates(text or '',ranked):reason.setdefault(c['id'],'rule')
73
+ return sorted(({**c,'pool_reason':reason[c['id']]} for c in ranked if c['id'] in reason),key=lambda c:c['start'])
74
+
75
+ _WITHDRAW=re.compile(r'withdr[ae]w|withdrawn|take back|disregard|rescind|retract|should not be relied|there is substituted|is deleted|expressly delete',re.I)
76
+ _REFERENCE=re.compile(r'(?:message of|wrote on|message dated) (\d{1,2} [A-Z][a-z]+)|minute (\d+)|[Ee]ntry (\d+) of [Ss]chedule (\d+)|clause ([\d.]+)|passage in my message')
77
+ _EXCEPTION=re.compile(r'does not apply to|subject to the exception|is an exception|except (?:where|that|for)\b',re.I)
78
+ _STOP={'this','that','these','those','with','under','which','what','does','file','correspondence','record','records','recorded',
79
+ 'minutes','agreement','bundle','messages','message','stand','stands','taking','reading','whole','strength','position',
80
+ 'open','given','have','been','applies','apply','entitled','allowed','liberty','free','from','there','their','they',
81
+ 'decisions','here','schedules','schedule','papers','office','shown','show','shows','case','matters','terms','place','placed'}
82
+ DEPENDENCIES_V1={'name':'withdrawal-exception-v1','withdrawals':True,'exceptions':True,'prune_withdrawn':False,'rare_df':16}
83
+
84
+ def _topic(words,exclude):
85
+ return {w for w in words if len(w)>=4 and w not in _STOP and w not in exclude and not w.isdigit()}
86
+
87
+ def _withdrawn_block(text,passage):
88
+ """Source range that a withdrawal refers to (dated message or numbered minute); None if not resolvable."""
89
+ m=_REFERENCE.search(passage)
90
+ if not m:return None
91
+ if m.group(1):head=re.search(r'Message \d+\. '+re.escape(m.group(1))+r'\.',text)
92
+ elif m.group(2):head=re.search(r'(?:^|\n)'+m.group(2)+r'\. ',text)
93
+ else:return None
94
+ if not head:return None
95
+ end=text.find('\n\n',head.end());return head.start(),(len(text) if end<0 else end)
96
+
97
+ def dependency_expand(question,spans,selected_ids,text,*,withdrawals=True,exceptions=True,prune_withdrawn=False,rare_df=16,**_):
98
+ """Complete a unit's selected evidence with its withdrawal/exception dependencies.
99
+
100
+ Source text and the already-selected passages only; never gold. Units with no
101
+ selection are unchanged, so no-positive-support decisions are preserved.
102
+ - withdrawal: a passage with a withdrawal verb AND an explicit reference (message/minute/
103
+ entry/clause) that names the question's subject (a rare capitalised name, also present in a
104
+ selected passage) and its most specific in-source topic word;
105
+ - exception: an exception clause immediately following a selected passage;
106
+ - prune_withdrawn (optional): drop selected passages inside the dated message / numbered
107
+ minute a kept withdrawal refers to, when they share its subject and a topic word.
108
+ Returns {'keep','added','pruned'} as candidate id lists in source order.
109
+ """
110
+ selected=[s for s in spans if s['id'] in set(selected_ids)]
111
+ if not selected:return {'keep':[],'added':[],'pruned':[]}
112
+ words=[_words(s['text']) for s in spans];df={}
113
+ for ws in words:
114
+ for w in ws:df[w]=df.get(w,0)+1
115
+ # Subject: capitalised non-initial question tokens (names) that are rare in the source.
116
+ names=re.findall(r'(?<!^)(?<![.?!] )\b[A-Z][\w&\'-]*',question.strip())
117
+ subject={w for n in names for w in _words(n) if len(w)>=3 and df.get(w,0)<=rare_df}
118
+ q=_words(question);present=[w for w in _topic(q,subject) if df.get(w,0)]
119
+ # The withdrawal must name the question's most specific in-source topic word (e.g. the attribute).
120
+ topic={min(present,key=lambda w:(df[w],w))} if present else set()
121
+ chosen={s['id'] for s in selected};sel_words=set().union(*(_words(s['text']) for s in selected));added=[]
122
+ for i,(s,ws) in enumerate(zip(spans,words)):
123
+ if s['id'] in chosen:continue
124
+ if (withdrawals and _WITHDRAW.search(s['text']) and _REFERENCE.search(s['text'])
125
+ and ws&subject&sel_words and ws&topic):added.append(s['id']);continue
126
+ if exceptions and i>0 and spans[i-1]['id'] in chosen and _EXCEPTION.search(s['text']):added.append(s['id'])
127
+ keep=chosen|set(added);pruned=[]
128
+ if prune_withdrawn:
129
+ for s,ws in zip(spans,words):
130
+ if s['id'] not in keep or not _WITHDRAW.search(s['text']):continue
131
+ block=_withdrawn_block(text,s['text'])
132
+ if block is None:continue
133
+ key=ws&subject;about=_topic(ws,subject)
134
+ for t,tw in zip(spans,words):
135
+ if (t['id'] in keep and t['id']!=s['id'] and block[0]<=t['start'] and t['end']<=block[1]+2
136
+ and tw&key and tw&about and not _WITHDRAW.search(t['text'])):pruned.append(t['id'])
137
+ order=[s['id'] for s in spans];pruned=set(pruned)
138
+ return {'keep':[i for i in order if i in keep and i not in pruned],'added':[i for i in order if i in set(added)],
139
+ 'pruned':[i for i in order if i in pruned]}
140
+
141
+ def lexical_select(text,questions,limit=4):
142
+ source=candidates(text);chosen={}
143
+ for q in questions:
144
+ for candidate in rank_candidates(q,source,limit):
145
+ if candidate['candidate_score']>0:chosen[candidate['id']]=candidate
146
+ return {'evidence':sorted(chosen.values(),key=lambda c:c['start']),
147
+ 'method':'lexical_overlap','verification':'retrieval_only','faithfulness_established':False}
148
+
149
+ def remove(text,spans):
150
+ spans=merge(text,spans);cursor=0;parts=[]
151
+ for s in spans:parts.append(text[cursor:s['start']]);cursor=s['end']
152
+ parts.append(text[cursor:]);return ''.join(parts)
mlx/src/solomon_mlx/_vendor/semantics.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Doccy Pty Ltd. Apache-2.0.
2
+ # Adapted from pinned Solomon v1.1; see NOTICE and MODIFICATIONS.md.
3
+ """Scope 9 answer semantics (docs/plans/2026-09-20-scope9-noul-final-refinement.md §1).
4
+
5
+ Yes/no, entity and multi-label candidates are Nouls: one probability P(yes). Gold yes only when the
6
+ document clearly establishes it; not stated and conflicting are no. Choice (single, ordered) is a
7
+ distribution over the listed options only; reserved-gold items have no Scope 9 target.
8
+
9
+ Works for both readouts: four_collapsed (four-state letter logits A/B/C/D, collapsed) and two_letter (A/B).
10
+ """
11
+ import numpy as np
12
+
13
+ YES, NO, NOT_STATED, CONFLICTING = 0, 1, 2, 3
14
+
15
+
16
+ def softmax(x, t=1.0):
17
+ z = np.asarray(x, np.float64) / t
18
+ z = z - z.max()
19
+ e = np.exp(z)
20
+ return e / e.sum()
21
+
22
+
23
+ def noul_gold(gold4):
24
+ """Four-state (or two-state) gold -> 1 for yes, 0 for no."""
25
+ return int(int(gold4) == YES)
26
+
27
+
28
+ def noul_logit(letter_logits):
29
+ """Binary log-odds z = log P(yes)/P(no) of a Noul branch: letter A against everything else."""
30
+ logits = np.asarray(letter_logits, np.float64)
31
+ if len(logits) not in (2, 4):
32
+ raise ValueError(f'Noul branch must have 2 or 4 letters, got {len(logits)}')
33
+ rest = logits[1:] - logits[1:].max()
34
+ return float(logits[YES] - (logits[1:].max() + np.log(np.exp(rest).sum())))
35
+
36
+
37
+ def p_yes(letter_logits, t=1.0):
38
+ """P(yes) from a Noul branch: letter A of a 2-letter (two_letter) or 4-state (four_collapsed) readout.
39
+
40
+ Temperature applies to the COLLAPSED binary logit, not to the letters: a Noul is a binary unit whose
41
+ 'no' mass may be spread over several reserved letters, so p_yes(t) = sigmoid(z/t) with z = noul_logit.
42
+ At t = 1 this is exactly softmax over the letters at A (the two forms only differ once t != 1, where the
43
+ letterwise form would decay toward 1/len(letters) instead of toward 1/2). scope9.qualification.p_yes and
44
+ abstention_refit/readout.py fit and evaluate the collapsed form, so the serving path must match it.
45
+ """
46
+ logits = np.asarray(letter_logits, np.float64)
47
+ if len(logits) not in (2, 4):
48
+ raise ValueError(f'Noul branch must have 2 or 4 letters, got {len(logits)}')
49
+ if t == 1.0:
50
+ return float(softmax(logits)[YES])
51
+ z = noul_logit(logits) / float(t)
52
+ return float(1.0 / (1.0 + np.exp(-z))) if z > -700 else 0.0
53
+
54
+
55
+ def noul_confidence(p):
56
+ return max(p, 1.0 - p)
57
+
58
+
59
+ def listed_gold(gold, n_options):
60
+ """Listed option index, or None when the old gold was a reserved slot (not stated / none-of-listed / conflicting)."""
61
+ return int(gold) if isinstance(gold, (int, np.integer)) and 0 <= int(gold) < n_options else None
62
+
63
+
64
+ def listed_probs(letter_logits, n_options, t=1.0):
65
+ """Choice distribution over the listed options only (reserved slots, if present, are discarded)."""
66
+ return softmax(np.asarray(letter_logits, np.float64)[:n_options], t)
67
+
68
+
69
+ def complement_deviation(p, p_negated):
70
+ """G3a under Scope 9: a statement and its negation should sum to 1."""
71
+ return abs(p - (1.0 - p_negated))
mlx/src/solomon_mlx/api.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public document-state API and four-state answer semantics."""
2
+
3
+ import copy
4
+ import json
5
+ import math
6
+ from pathlib import Path
7
+
8
+ from ._vendor.contract import _state_parts, decision, parse_questions, present
9
+ from ._vendor.prompts import boolean_block, label_block, listwise_block
10
+ from ._vendor.semantics import listed_probs, p_yes
11
+ from .artifacts import digest, sha256
12
+
13
+ TASKS = ("boolean", "single", "ordered", "multilabel", "entity")
14
+
15
+
16
+ def branches(spec):
17
+ task, req = spec["task"], spec["request"]
18
+ if task == "boolean":
19
+ return [(boolean_block(req["question"]), 4, "boolean/state4")]
20
+ if task in ("single", "ordered"):
21
+ block, width = listwise_block(
22
+ req["question"], req["options"], ordered=task == "ordered", reserved=task == "single"
23
+ )
24
+ return [(block, width, task + ("/choiceR" if task == "single" else "/choiceS"))]
25
+ if task == "entity":
26
+ return [
27
+ (boolean_block(req["template"].replace("{entity}", c)), 4, "entity/state4")
28
+ for c in req["entities"]
29
+ ]
30
+ return [(*label_block(req["question"], c), "multilabel/state4") for c in req["labels"]]
31
+
32
+
33
+ def distributions(spec, rows, temperature):
34
+ if spec["task"] in ("boolean", "entity", "multilabel"):
35
+ values = [p_yes(r["letter_logits"], temperature) for r in rows]
36
+ return [[p, 1 - p] for p in values]
37
+ return [listed_probs(rows[0]["letter_logits"], len(spec["texts"]), temperature).tolist()]
38
+
39
+
40
+ def ordering_score(values):
41
+ """Product of per-unit top probabilities; not a calibrated joint probability."""
42
+ if not values:
43
+ raise ValueError("At least one answer unit is required")
44
+ for p in values:
45
+ if len(p) < 2 or not all(math.isfinite(v) and v >= 0 for v in p) or abs(sum(p) - 1) > 1e-6:
46
+ raise ValueError("Invalid answer distribution")
47
+ return math.prod(max(p) for p in values)
48
+
49
+
50
+ class DocumentState:
51
+ def __init__(self, owner, data):
52
+ self._owner, self._data, self.closed = owner, data, False
53
+ self.image_hashes = {p["image"]: sha256(p["image"]) for p in data["parts"] if "image" in p}
54
+
55
+ @property
56
+ def prefix_tokens(self):
57
+ self._check()
58
+ return len(self._data["prefix_ids"])
59
+
60
+ def _check(self):
61
+ if self.closed:
62
+ raise ValueError("Document state is closed")
63
+ if any(sha256(path) != value for path, value in self.image_hashes.items()):
64
+ raise ValueError("Document image changed after prefill")
65
+
66
+ def save(self, path):
67
+ """Save a source-bound replay recipe, never pickle executable cache objects."""
68
+ self._check()
69
+ body = {
70
+ "format": "solomon-mlx-replay-v1",
71
+ "runtime": self._owner.identity["fingerprint"],
72
+ "parts": self._data["parts"],
73
+ "image_hashes": self.image_hashes,
74
+ "prefix_ids_sha256": digest(self._data["prefix_ids"]),
75
+ }
76
+ Path(path).write_text(json.dumps({**body, "sha256": digest(body)}, indent=2))
77
+
78
+ def close(self):
79
+ with self._owner.engine.lock:
80
+ self._data.clear()
81
+ self.closed = True
82
+
83
+ def __enter__(self):
84
+ self._check()
85
+ return self
86
+
87
+ def __exit__(self, *args):
88
+ self.close()
89
+
90
+
91
+ class Solomon:
92
+ @classmethod
93
+ def load(
94
+ cls,
95
+ model_dir,
96
+ profile="quality",
97
+ *,
98
+ chunk_size=2048,
99
+ max_tokens=40960,
100
+ page_selector=None,
101
+ calibration=None,
102
+ ):
103
+ if profile != "quality":
104
+ raise ValueError("Only full BF16 quality is implemented; quantization is secondary")
105
+ from .engine import Engine
106
+
107
+ return cls(
108
+ Engine(model_dir, chunk_size=chunk_size, max_tokens=max_tokens),
109
+ page_selector=page_selector,
110
+ calibration=calibration,
111
+ )
112
+
113
+ def __init__(self, engine, *, page_selector=None, calibration=None):
114
+ self.engine, self.identity, self.page_selector = engine, engine.identity, page_selector
115
+ self.temperatures = dict.fromkeys(TASKS, 1.0)
116
+ self.calibration_status = "uncalibrated"
117
+ if calibration is not None:
118
+ artifact = json.loads(Path(calibration).read_text())
119
+ payload = {k: v for k, v in artifact.items() if k != "sha256"}
120
+ if (
121
+ artifact.get("sha256") != digest(payload)
122
+ or artifact["runtime"] != self.identity["fingerprint"]
123
+ ):
124
+ raise ValueError("Calibration checksum or MLX runtime identity mismatch")
125
+ temps = artifact["temperatures"]
126
+ if set(temps) != set(TASKS) or any(
127
+ isinstance(v, bool)
128
+ or not isinstance(v, (int, float))
129
+ or not math.isfinite(v)
130
+ or not 0 < v <= 20
131
+ for v in temps.values()
132
+ ):
133
+ raise ValueError("Invalid temperatures")
134
+ self.temperatures, self.calibration_status = temps, "profile_fitted"
135
+
136
+ def prefill(self, document):
137
+ parts = copy.deepcopy(_state_parts(document))
138
+ if not parts:
139
+ parts = [{"text": ""}]
140
+ for p in parts:
141
+ if not isinstance(p, dict) or set(p) not in ({"text"}, {"image"}):
142
+ raise ValueError("Each document part must contain only text or image")
143
+ if "text" in p and not isinstance(p["text"], str):
144
+ raise ValueError("Text parts must be strings")
145
+ if "image" in p:
146
+ p["image"] = str(Path(p["image"]).resolve(strict=True))
147
+ hashes = {p["image"]: sha256(p["image"]) for p in parts if "image" in p}
148
+ state = DocumentState(self, self.engine.prefill(parts))
149
+ if state.image_hashes != hashes:
150
+ state.close()
151
+ raise ValueError("Image changed while document was being prefilled")
152
+ return state
153
+
154
+ def replay(self, path):
155
+ body = json.loads(Path(path).read_text())
156
+ expected = body.pop("sha256")
157
+ if (
158
+ digest(body) != expected
159
+ or body["format"] != "solomon-mlx-replay-v1"
160
+ or body["runtime"] != self.identity["fingerprint"]
161
+ ):
162
+ raise ValueError("Replay checksum or runtime mismatch")
163
+ if any(sha256(p) != h for p, h in body["image_hashes"].items()):
164
+ raise ValueError("Replay image changed")
165
+ state = self.prefill(body["parts"])
166
+ if digest(state._data["prefix_ids"]) != body["prefix_ids_sha256"]:
167
+ state.close()
168
+ raise ValueError("Replay tokenization differs")
169
+ return state
170
+
171
+ def _answer(self, state, spec, execution="cached"):
172
+ rows = [self.engine.ask(state._data, b, n, h, execution=execution) for b, n, h in branches(spec)]
173
+ dists = distributions(spec, rows, self.temperatures[spec["task"]])
174
+ return {
175
+ **present(spec, dists),
176
+ "ordering_score": ordering_score(dists),
177
+ "temperature": self.temperatures[spec["task"]],
178
+ }, rows
179
+
180
+ def decide(
181
+ self,
182
+ *,
183
+ state,
184
+ questions,
185
+ evidence="support",
186
+ evidence_max_calls=64,
187
+ execution="cached",
188
+ diagnostics=False,
189
+ ):
190
+ if not isinstance(state, DocumentState) or state._owner is not self:
191
+ raise ValueError("State belongs to a different model instance")
192
+ if evidence not in ("none", "support", "sufficiency", "removal"):
193
+ raise ValueError("Invalid evidence level")
194
+ if type(evidence_max_calls) is not int or not 0 <= evidence_max_calls <= 512:
195
+ raise ValueError("Invalid evidence call budget")
196
+ specs = parse_questions(questions)
197
+ with self.engine.lock:
198
+ state._check()
199
+ answers, usage = {}, {"branches": 0, "input_tokens": 0, "evidence_calls": 0}
200
+ for spec in specs:
201
+ answer, rows = self._answer(state, spec, execution)
202
+ body = self._evidence(state, spec, answer, evidence, evidence_max_calls)
203
+ answer.update(
204
+ evidence=body["references"], evidence_status=body["status"], evidence_detail=body
205
+ )
206
+ if diagnostics:
207
+ answer["branches"] = rows
208
+ answers[spec["id"]] = answer
209
+ usage["branches"] += len(rows)
210
+ usage["input_tokens"] += sum(
211
+ r["branch_tokens"] if execution == "cached" else r["prompt_tokens"] for r in rows
212
+ )
213
+ usage["evidence_calls"] += body.get("calls", 0)
214
+ return {
215
+ "answers": answers,
216
+ "usage": usage,
217
+ "runtime": self.identity,
218
+ "calibration_status": self.calibration_status,
219
+ "answer_policy": "always_answers",
220
+ }
221
+
222
+ def _fresh(self, document, spec):
223
+ with self.prefill(document) as state:
224
+ answer, _ = self._answer(state, spec)
225
+ return decision(spec, answer)
226
+
227
+ def _evidence(self, state, spec, answer, level, budget):
228
+ from ._vendor import evidence_v3 as v3
229
+ from ._vendor.evidence import image_pages, validate_pages, validate_spans
230
+ from ._vendor.retrieval import lexical_select, remove
231
+
232
+ body = {
233
+ "references": [],
234
+ "status": "not_requested",
235
+ "calls": 0,
236
+ "verification": "none",
237
+ "faithfulness_established": False,
238
+ }
239
+ if level == "none":
240
+ return body
241
+ parts, req = state._data["parts"], spec["request"]
242
+ task = spec["task"]
243
+ if task == "entity":
244
+ questions = [req["template"].replace("{entity}", c) for c in req["entities"]]
245
+ elif task == "multilabel":
246
+ questions = [req["question"] + " Label: " + c for c in req["labels"]]
247
+ else:
248
+ questions = [req["question"] + (" " + " ".join(req["options"]) if "options" in req else "")]
249
+ images = [p["image"] for p in parts if "image" in p]
250
+ needed = (1 if images else len(questions)) if level in ("sufficiency", "removal") else 0
251
+ needed += int(level == "removal")
252
+ if needed > budget:
253
+ return {**body, "status": "budget_exhausted", "required_calls": needed}
254
+ baseline = decision(spec, answer)
255
+ if images:
256
+ if self.page_selector is None:
257
+ return {**body, "status": "unsupported_page_selector", "pages_available": len(images)}
258
+ pages = image_pages(images)
259
+ selector = self.page_selector
260
+ plan = None
261
+ if hasattr(selector, "plan"):
262
+ plan = selector.plan(
263
+ pages, questions, **({"task": task} if getattr(selector, "task_aware", False) else {})
264
+ )
265
+ if type(plan.get("calls")) is not int or plan["calls"] < 0:
266
+ raise ValueError("Invalid page selector call estimate")
267
+ if needed + plan["calls"] > budget:
268
+ return {**body, "status": "budget_exhausted", "required_calls": needed + plan["calls"]}
269
+ selection = (
270
+ selector.execute(plan) if plan is not None else selector(copy.deepcopy(pages), questions)
271
+ )
272
+ calls = selection.get("cost", {}).get("calls", 0)
273
+ if calls != (plan["calls"] if plan is not None else 0):
274
+ raise ValueError("Page selector exceeded its declared call budget")
275
+ refs = validate_pages(pages, selection["evidence"])
276
+ body["calls"] = calls
277
+ selected = {r["page"] for r in refs}
278
+ page, remainder = 0, []
279
+ for part in parts:
280
+ if "image" in part:
281
+ page += 1
282
+ if page in selected:
283
+ continue
284
+ remainder.append(part)
285
+ subsets = [([{"image": r["path"]} for r in refs], spec)]
286
+ else:
287
+ text = "".join(p["text"] for p in parts)
288
+ selection = lexical_select(text, questions)
289
+ refs = validate_spans(text, selection["evidence"])
290
+ structure = v3.Structure(text)
291
+ packages, subsets = [], []
292
+ for i, q in enumerate(questions):
293
+ subject = req["entities"][i] if task == "entity" else None
294
+ package = v3.build(text, q, refs, subject=subject, structure=structure)
295
+ unit = copy.deepcopy(spec)
296
+ if "candidates" in spec:
297
+ candidate = spec["candidates"][i]
298
+ unit["candidates"] = [candidate]
299
+ unit["request"]["entities" if task == "entity" else "labels"] = [candidate]
300
+ subsets.append((package["text"], unit))
301
+ packages.append({k: v for k, v in package.items() if k != "text"})
302
+ body["packages"] = packages
303
+ remainder = remove(text, refs)
304
+ body.update(
305
+ references=refs, status="found" if refs else "no_support_found", verification="retrieval_only"
306
+ )
307
+ if level in ("sufficiency", "removal"):
308
+ predictions = [self._fresh(doc, unit) for doc, unit in subsets]
309
+ assembled = (
310
+ {k: v for d in predictions for k, v in d.items()}
311
+ if "candidates" in spec and not images
312
+ else predictions[0]
313
+ )
314
+ body["evidence_only"] = {"prediction": assembled, "agrees_with_full": assembled == baseline}
315
+ body["calls"] += len(subsets)
316
+ body["verification"] = "fresh_source_reencoding"
317
+ if level == "removal":
318
+ removed = self._fresh(remainder, spec)
319
+ body["evidence_removed"] = {"prediction": removed, "agrees_with_full": removed == baseline}
320
+ body["calls"] += 1
321
+ return body
mlx/src/solomon_mlx/artifacts.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Artifact verification and distinct MLX runtime identities."""
2
+
3
+ import hashlib
4
+ import json
5
+ from importlib.metadata import version
6
+ from pathlib import Path
7
+
8
+ SOLOMON_REVISION = "2ec506902269e4636285c8811f6c0f52c9300c0c"
9
+ BASE_REVISION = "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0"
10
+ ADAPTER_SHA = "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca"
11
+ HEADS_SHA = "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f"
12
+
13
+
14
+ def sha256(path):
15
+ h = hashlib.sha256()
16
+ with Path(path).open("rb") as f:
17
+ for block in iter(lambda: f.read(8 << 20), b""):
18
+ h.update(block)
19
+ return h.hexdigest()
20
+
21
+
22
+ def digest(value):
23
+ return hashlib.sha256(
24
+ json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode()
25
+ ).hexdigest()
26
+
27
+
28
+ def verify_release(root):
29
+ root = Path(root)
30
+ manifest = json.loads((root / "MANIFEST.json").read_text())
31
+ checked = {}
32
+ for row in manifest["files"]:
33
+ rel = row["destination"]
34
+ path = (root / rel).resolve()
35
+ if not path.is_relative_to(root.resolve()):
36
+ raise ValueError("Manifest path escapes source directory")
37
+ expected = row.get("staged_sha256")
38
+ size = row.get("staged_bytes")
39
+ actual = sha256(path)
40
+ if expected and actual != expected:
41
+ raise ValueError(f"Source checksum mismatch: {rel}")
42
+ if size is not None and path.stat().st_size != size:
43
+ raise ValueError(f"Source size mismatch: {rel}")
44
+ checked[rel] = actual
45
+ return checked
46
+
47
+
48
+ def code_identity():
49
+ root = Path(__file__).parent
50
+ return digest({str(p.relative_to(root)): sha256(p) for p in sorted(root.rglob("*.py"))})
51
+
52
+
53
+ def runtime_identity(binding, *, chunk_size=2048, max_tokens=40960):
54
+ import platform
55
+
56
+ import mlx.core as mx
57
+
58
+ value = {
59
+ "backend": "mlx-metal",
60
+ "contract": "solomon-mlx-v1",
61
+ "source_contract": "solomon-v1",
62
+ "profile": binding["profile"],
63
+ "chunk_size": chunk_size,
64
+ "max_tokens": max_tokens,
65
+ "metal_device": mx.device_info(),
66
+ "macos_version": platform.mac_ver()[0],
67
+ "model_binding": digest(binding),
68
+ "code_sha256": code_identity(),
69
+ "versions": {p: version(p) for p in ("mlx", "mlx-vlm", "transformers", "numpy", "pillow")},
70
+ "placement": "question",
71
+ "lora_scale": 2.0,
72
+ "answer_projection": "trained-semantic-head-float32",
73
+ "recurrence": "mlx-float32",
74
+ "solomon_revision": SOLOMON_REVISION,
75
+ "base_revision": BASE_REVISION,
76
+ }
77
+ return {**value, "fingerprint": digest(value)}
mlx/src/solomon_mlx/budget.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fail-closed local cost reservations for serialized Modal reference jobs."""
2
+
3
+ import fcntl
4
+ import json
5
+ import math
6
+ import os
7
+ import time
8
+ import uuid
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+
12
+
13
+ class Budget:
14
+ def __init__(self, path, cap=250.0):
15
+ self.path, self.cap = Path(path), cap
16
+ if not math.isfinite(cap) or not 0 < cap <= 250:
17
+ raise ValueError("The authorized cap is at most US$250")
18
+
19
+ @contextmanager
20
+ def reserve(self, amount, label):
21
+ if not math.isfinite(amount) or amount <= 0:
22
+ raise ValueError("Positive finite reservation required")
23
+ self.path.parent.mkdir(parents=True, exist_ok=True)
24
+ with self.path.with_suffix(".lock").open("a") as lock:
25
+ # Held until the synchronous remote call finishes. Refuse concurrent jobs.
26
+ try:
27
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
28
+ except BlockingIOError:
29
+ raise RuntimeError("Another reference job already owns the GPU budget") from None
30
+ ledger = (
31
+ json.loads(self.path.read_text()) if self.path.exists() else {"cap": self.cap, "entries": []}
32
+ )
33
+ if ledger["cap"] != self.cap:
34
+ raise ValueError("Budget cap changed")
35
+ if any(e["status"] == "reserved" for e in ledger["entries"]):
36
+ raise RuntimeError(
37
+ "Unresolved prior dispatch: reconcile its remote status before another GPU job"
38
+ )
39
+ if any(
40
+ type(e.get("reserved_usd")) not in (int, float)
41
+ or not math.isfinite(e["reserved_usd"])
42
+ or e["reserved_usd"] <= 0
43
+ or e.get("status") not in ("reserved", "completed_conservative_charge")
44
+ for e in ledger["entries"]
45
+ ):
46
+ raise ValueError("Corrupt budget ledger")
47
+ used = sum(e["reserved_usd"] for e in ledger["entries"])
48
+ if used + amount > self.cap:
49
+ raise RuntimeError("Modal budget exhausted before dispatch")
50
+ entry = {
51
+ "id": uuid.uuid4().hex,
52
+ "label": label,
53
+ "reserved_usd": amount,
54
+ "status": "reserved",
55
+ "time": time.time(),
56
+ }
57
+ ledger["entries"].append(entry)
58
+ self._write(ledger)
59
+ # If interrupted, leave the reservation unresolved.
60
+ yield entry
61
+ entry["status"] = "completed_conservative_charge"
62
+ entry["completed"] = time.time()
63
+ self._write(ledger)
64
+
65
+ def _write(self, value):
66
+ tmp = self.path.with_suffix(".tmp")
67
+ with tmp.open("w") as file:
68
+ file.write(json.dumps(value, indent=2))
69
+ file.flush()
70
+ os.fsync(file.fileno())
71
+ tmp.replace(self.path)
mlx/src/solomon_mlx/cli.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+
6
+ def main():
7
+ parser = argparse.ArgumentParser(description="Private Solomon BF16 MLX tooling")
8
+ commands = parser.add_subparsers(dest="command", required=True)
9
+ verify = commands.add_parser("verify-source")
10
+ verify.add_argument("directory")
11
+ download = commands.add_parser("download-base")
12
+ download.add_argument("--output", default="snapshots/base")
13
+ convert = commands.add_parser("prepare")
14
+ convert.add_argument("--base", default="snapshots/base")
15
+ convert.add_argument("--solomon", default="snapshots/solomon")
16
+ convert.add_argument("--manifest", default="snapshots/base-manifest.json")
17
+ convert.add_argument("--output", default="models/quality")
18
+ decide = commands.add_parser("decide")
19
+ decide.add_argument("--model", default="models/quality")
20
+ decide.add_argument("--document", required=True)
21
+ decide.add_argument("--questions", required=True)
22
+ decide.add_argument(
23
+ "--evidence", choices=["none", "support", "sufficiency", "removal"], default="support"
24
+ )
25
+ args = parser.parse_args()
26
+ if args.command == "verify-source":
27
+ from .artifacts import verify_release
28
+
29
+ print(json.dumps(verify_release(args.directory), indent=2))
30
+ elif args.command == "download-base":
31
+ from huggingface_hub import HfApi, snapshot_download
32
+
33
+ from .artifacts import BASE_REVISION
34
+ from .prepare import verify_base
35
+
36
+ model = HfApi().model_info("Qwen/Qwen3.8-27B", revision=BASE_REVISION, files_metadata=True)
37
+ manifest = {
38
+ "revision": model.sha,
39
+ "files": [
40
+ {
41
+ "name": f.rfilename,
42
+ "size": f.size,
43
+ "blob_id": f.blob_id,
44
+ "sha256": f.lfs.sha256 if f.lfs else None,
45
+ }
46
+ for f in model.siblings
47
+ ],
48
+ }
49
+ Path(args.output).parent.mkdir(parents=True, exist_ok=True)
50
+ Path(args.output + "-manifest.json").write_text(json.dumps(manifest, indent=2))
51
+ snapshot_download("Qwen/Qwen3.8-27B", revision=BASE_REVISION, local_dir=args.output, max_workers=18)
52
+ verified = verify_base(args.output, manifest)
53
+ Path(args.output + "-verified.json").write_text(json.dumps(verified, indent=2))
54
+ elif args.command == "prepare":
55
+ from .prepare import prepare
56
+
57
+ prepare(args.base, args.solomon, args.output, args.manifest)
58
+ else:
59
+ from .api import Solomon
60
+
61
+ model = Solomon.load(args.model)
62
+ with model.prefill(Path(args.document).read_text()) as state:
63
+ print(
64
+ json.dumps(
65
+ model.decide(
66
+ state=state,
67
+ questions=json.loads(Path(args.questions).read_text()),
68
+ evidence=args.evidence,
69
+ ),
70
+ indent=2,
71
+ )
72
+ )
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
mlx/src/solomon_mlx/engine.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BF16 Metal execution with instance-owned adaptation and isolated question caches."""
2
+
3
+ import copy
4
+ import json
5
+ import threading
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import mlx.core as mx
10
+ import numpy as np
11
+ from mlx import nn
12
+ from PIL import Image
13
+
14
+ from ._vendor.prompts import PAGE, SYSTEM
15
+ from .artifacts import ADAPTER_SHA, BASE_REVISION, HEADS_SHA, SOLOMON_REVISION, runtime_identity, sha256
16
+
17
+
18
+ class SwitchLoRA(nn.Module):
19
+ def __init__(self, linear, a, b, context):
20
+ super().__init__()
21
+ self.linear, self.lora_a, self.lora_b = linear, a, b
22
+ self._context = context
23
+
24
+ def __call__(self, x):
25
+ y = self.linear(x)
26
+ start = self._context["start"]
27
+ if start is None or start >= x.shape[1]:
28
+ return y
29
+ delta = (2.0 * ((x[:, start:].astype(mx.float32) @ self.lora_a) @ self.lora_b)).astype(y.dtype)
30
+ return y + delta if start == 0 else mx.concatenate([y[:, :start], y[:, start:] + delta], axis=1)
31
+
32
+
33
+ def fork_cache(caches):
34
+ """New cache containers and array handles; MLX owns copy-on-write storage.
35
+
36
+ mx.array creates a distinct handle, so slice updates cannot change a prefix's
37
+ Python array. Recurrent/window updates replace the branch's private slots.
38
+ """
39
+ from mlx_vlm.models.cache import ArraysCache, KVCache
40
+
41
+ result = []
42
+ for original in caches:
43
+ if isinstance(original, ArraysCache):
44
+ branch = ArraysCache(len(original.cache))
45
+ branch.cache = [None if x is None else mx.array(x) for x in original.cache]
46
+ elif isinstance(original, KVCache):
47
+ branch = KVCache()
48
+ branch.state = tuple(None if x is None else mx.array(x) for x in original.state)
49
+ else:
50
+ raise TypeError(f"Unsupported prefix cache: {type(original).__name__}")
51
+ result.append(branch)
52
+ return result
53
+
54
+
55
+ class Engine:
56
+ def __init__(self, directory, *, chunk_size=2048, max_tokens=40960):
57
+ from mlx_vlm.models.qwen3_vl.processing_qwen3_vl import Qwen3VLProcessor
58
+ from mlx_vlm.utils import load_model
59
+
60
+ self.directory = Path(directory).resolve()
61
+ self.binding = json.loads((self.directory / "binding.json").read_text())
62
+ if (
63
+ self.binding.get("schema") != "solomon-mlx-binding-v1"
64
+ or self.binding.get("base_revision") != BASE_REVISION
65
+ or self.binding.get("solomon_revision") != SOLOMON_REVISION
66
+ ):
67
+ raise ValueError("Unrecognized or unpinned Solomon MLX binding")
68
+ if self.binding["profile"] != "quality" or self.binding["dtype"] != "bfloat16":
69
+ raise ValueError("This runtime currently accepts only the BF16 quality profile")
70
+ for name, expected in self.binding["files"].items():
71
+ path = (self.directory / name).resolve()
72
+ if not path.is_relative_to(self.directory) or sha256(path) != expected:
73
+ raise ValueError(f"Model artifact checksum mismatch: {name}")
74
+ adapter, heads = self.directory / "adapter.safetensors", self.directory / "heads.npz"
75
+ if sha256(adapter) != ADAPTER_SHA or sha256(heads) != HEADS_SHA:
76
+ raise ValueError("Solomon checkpoint identity mismatch")
77
+ if not 1 <= chunk_size <= 2048 or not 1 <= max_tokens <= 40960:
78
+ raise ValueError("Invalid chunk size or context ceiling")
79
+ weight_bytes = sum(
80
+ (self.directory / name).stat().st_size
81
+ for name in self.binding["files"]
82
+ if name.endswith((".safetensors", ".npz"))
83
+ )
84
+ if weight_bytes + 4 * 2**30 > mx.device_info()["max_recommended_working_set_size"]:
85
+ raise MemoryError(
86
+ "Full BF16 weights and minimum workspace exceed this Mac’s recommended Metal working set"
87
+ )
88
+ self.chunk_size, self.max_tokens = chunk_size, max_tokens
89
+ self.lock = threading.RLock()
90
+ self.context = {"start": None}
91
+ self.model = load_model(self.directory / "backbone", lazy=True, strict=True)
92
+ self.processor = Qwen3VLProcessor.from_pretrained(
93
+ str(self.directory / "backbone"), trust_remote_code=False
94
+ )
95
+ self.lm, self.t = self.model.language_model, self.processor.tokenizer
96
+ self.pad = self.t.convert_tokens_to_ids("<|image_pad|>")
97
+ weights = mx.load(str(adapter))
98
+ for name in sorted({key.rsplit(".", 1)[0] for key in weights}):
99
+ parts = name.split(".")
100
+ if parts[:2] != ["model", "layers"]:
101
+ raise ValueError(f"Unexpected adapter target: {name}")
102
+ owner = self.lm.model.layers[int(parts[2])]
103
+ for part in parts[3:-1]:
104
+ owner = getattr(owner, part)
105
+ linear = getattr(owner, parts[-1])
106
+ a, b = weights[name + ".lora_a"].astype(mx.float32), weights[name + ".lora_b"].astype(mx.float32)
107
+ if a.shape != (linear.weight.shape[1], 64) or b.shape != (64, linear.weight.shape[0]):
108
+ raise ValueError(f"Adapter orientation/shape mismatch: {name}")
109
+ setattr(owner, parts[-1], SwitchLoRA(linear, a, b, self.context))
110
+ with np.load(heads, allow_pickle=False) as archive:
111
+ keys = {k[:-7] for k in archive.files if k.endswith("/weight")}
112
+ required_heads = {
113
+ "boolean/state4",
114
+ "entity/state4",
115
+ "multilabel/state4",
116
+ "ordered/threshold4",
117
+ "single/choiceR",
118
+ "single/choiceS",
119
+ "single/sufficiency3",
120
+ "ordered/choiceR",
121
+ "ordered/choiceS",
122
+ "ordered/sufficiency3",
123
+ }
124
+ if keys != required_heads:
125
+ raise ValueError("All ten semantic heads are required")
126
+ self.heads = {}
127
+ for key in keys:
128
+ w, b = archive[key + "/weight"], archive[key + "/bias"]
129
+ if (
130
+ w.shape != (10, 5120)
131
+ or b.shape != (10,)
132
+ or not np.isfinite(w).all()
133
+ or not np.isfinite(b).all()
134
+ ):
135
+ raise ValueError("Invalid semantic head")
136
+ self.heads[key] = (mx.array(w, mx.float32), mx.array(b, mx.float32))
137
+ self.model.freeze()
138
+ self.model.eval()
139
+ mx.eval(self.model.parameters(), self.heads)
140
+ self.identity = runtime_identity(self.binding, chunk_size=self.chunk_size, max_tokens=self.max_tokens)
141
+
142
+ def render(self, parts, block):
143
+ content = ""
144
+ for i, p in enumerate(parts):
145
+ if "text" in p:
146
+ content += ("\n" if i and "image" in parts[i - 1] else "") + p["text"]
147
+ else:
148
+ content += ("\n" if i and "text" in parts[i - 1] else "") + PAGE
149
+ return self.t.apply_chat_template(
150
+ [
151
+ {"role": "system", "content": SYSTEM},
152
+ {"role": "user", "content": "Document:\n" + content + "\n\n" + block},
153
+ ],
154
+ tokenize=False,
155
+ add_generation_prompt=True,
156
+ enable_thinking=False,
157
+ )
158
+
159
+ def expand(self, ids, counts):
160
+ out, index = [], 0
161
+ for token in ids:
162
+ if token == self.pad:
163
+ if index >= len(counts):
164
+ raise ValueError("Unexpected image placeholder in document text")
165
+ out.extend([token] * counts[index])
166
+ index += 1
167
+ else:
168
+ out.append(token)
169
+ if index != len(counts):
170
+ raise ValueError("Image placeholder count mismatch")
171
+ return out
172
+
173
+ def positions(self, start, count):
174
+ return mx.broadcast_to(mx.arange(start, start + count)[None, None, :], (3, 1, count))
175
+
176
+ def admit(self, count):
177
+ if count < 1 or count > self.max_tokens:
178
+ raise ValueError(f"{count} tokens exceeds the {self.max_tokens}-token scope ceiling")
179
+ # Conservative allowance: BF16 attention KV + FP32 recurrent states and
180
+ # chunk intermediates. This supplements the token ceiling, not a promise
181
+ # of availability in the presence of other processes.
182
+ temporary = 4 * 2**30 + count * 16 * 2 * 4 * 256 * 2
183
+ limit = mx.device_info()["max_recommended_working_set_size"]
184
+ if mx.get_active_memory() + temporary > limit:
185
+ raise MemoryError("Insufficient recommended Metal working set for this request")
186
+
187
+ def forward(self, ids, positions, cache, *, embeds=None, adapter_from=None, taps=()):
188
+ hidden, captured = None, {}
189
+ try:
190
+ for start in range(0, len(ids), self.chunk_size):
191
+ end = min(start + self.chunk_size, len(ids))
192
+ self.context["start"] = None if adapter_from is None else max(0, adapter_from - start)
193
+ last = end == len(ids)
194
+ out = self.lm(
195
+ mx.array([ids[start:end]]),
196
+ cache=cache,
197
+ position_ids=positions[:, :, start:end],
198
+ inputs_embeds=None if embeds is None else embeds[:, start:end],
199
+ skip_logits=True,
200
+ return_hidden=last,
201
+ capture_layer_ids=list(taps) if last else None,
202
+ )
203
+ if last:
204
+ hidden = out.hidden_states[-1][0, -1].astype(mx.float32)
205
+ captured = {
206
+ str(i): self.lm.model.norm(h[:, -1:])[0, -1].astype(mx.float32)
207
+ for i, h in zip(sorted(set(taps)), out.hidden_states[:-1])
208
+ }
209
+ mx.eval(hidden, captured)
210
+ mx.eval([c.state for c in cache])
211
+ return hidden, captured
212
+ finally:
213
+ self.context["start"] = None
214
+
215
+ def prefill(self, parts):
216
+ with self.lock:
217
+ prefill_started = time.perf_counter()
218
+ text = self.render(parts, "X")
219
+ boundary = text.rfind("\n\nX")
220
+ if boundary < 0:
221
+ raise ValueError("Missing document boundary")
222
+ raw = self.t.encode(text[:boundary], add_special_tokens=False)
223
+ if "text" in parts[-1]:
224
+ raw = raw[:-1]
225
+ vision_started = time.perf_counter()
226
+ counts, grids, features = [], [], []
227
+ for part in parts:
228
+ if "image" not in part:
229
+ continue
230
+ with Image.open(part["image"]) as image:
231
+ processed = self.processor.image_processor(images=[image.convert("RGB")])
232
+ grid_np = np.asarray(processed["image_grid_thw"])
233
+ count = int(grid_np.prod()) // self.model.config.vision_config.spatial_merge_size**2
234
+ self.admit(len(raw) + sum(counts) + count - len(counts) - 1)
235
+ grid = mx.array(grid_np)
236
+ pixels = mx.array(np.asarray(processed["pixel_values"])).astype(
237
+ self.model.vision_tower.patch_embed.proj.weight.dtype
238
+ )
239
+ feature, _ = self.model.vision_tower(pixels, grid)
240
+ mx.eval(feature)
241
+ counts.append(count)
242
+ grids.append(grid)
243
+ features.append(feature)
244
+ vision_seconds = time.perf_counter() - vision_started if counts else 0.0
245
+ ids = self.expand(raw, counts)
246
+ self.admit(len(ids))
247
+ embeds, delta, feats, grid = None, 0, None, None
248
+ if counts:
249
+ feats, grid = mx.concatenate(features), mx.concatenate(grids)
250
+ f = self.model.get_input_embeddings(
251
+ mx.array([ids]), mx.zeros((1,)), image_grid_thw=grid, cached_image_features=feats
252
+ )
253
+ embeds, positions = f.inputs_embeds, f.position_ids
254
+ delta = int(np.asarray(f.rope_deltas).reshape(-1)[0])
255
+ if delta != int(mx.max(positions).item()) + 1 - len(ids):
256
+ raise ValueError("Multimodal RoPE offset mismatch")
257
+ else:
258
+ positions = self.positions(0, len(ids))
259
+ cache = self.lm.make_cache()
260
+ started = time.perf_counter()
261
+ self.forward(ids, positions, cache, embeds=embeds)
262
+ return {
263
+ "parts": copy.deepcopy(parts),
264
+ "prefix_ids": ids,
265
+ "cache": cache,
266
+ "counts": counts,
267
+ "rope_delta": delta,
268
+ "features": feats,
269
+ "grid": grid,
270
+ "positions": positions,
271
+ "prefill_seconds": time.perf_counter() - prefill_started,
272
+ "language_prefill_seconds": time.perf_counter() - started,
273
+ "vision_seconds": vision_seconds,
274
+ }
275
+
276
+ def ask(self, state, block, width, head, *, execution="cached", taps=()):
277
+ with self.lock:
278
+ if head not in self.heads or not 2 <= width <= 10:
279
+ raise ValueError("Unknown semantic head or invalid width")
280
+ ids = self.expand(
281
+ self.t.encode(self.render(state["parts"], block), add_special_tokens=False), state["counts"]
282
+ )
283
+ self.admit(len(ids))
284
+ p = len(state["prefix_ids"])
285
+ if ids[:p] != state["prefix_ids"]:
286
+ raise ValueError("Question token prefix differs from cached document")
287
+ started = time.perf_counter()
288
+ if execution == "cached":
289
+ hidden, captured = self.forward(
290
+ ids[p:],
291
+ self.positions(p + state["rope_delta"], len(ids) - p),
292
+ fork_cache(state["cache"]),
293
+ adapter_from=0,
294
+ taps=taps,
295
+ )
296
+ elif execution == "full":
297
+ embeds = None
298
+ if state["counts"]:
299
+ f = self.model.get_input_embeddings(
300
+ mx.array([ids]),
301
+ mx.zeros((1,)),
302
+ image_grid_thw=state["grid"],
303
+ cached_image_features=state["features"],
304
+ )
305
+ embeds, positions = f.inputs_embeds, f.position_ids
306
+ else:
307
+ positions = self.positions(0, len(ids))
308
+ hidden, captured = self.forward(
309
+ ids, positions, self.lm.make_cache(), embeds=embeds, adapter_from=p, taps=taps
310
+ )
311
+ else:
312
+ raise ValueError("Execution must be cached or full")
313
+ w, b = self.heads[head]
314
+ logits = (w @ hidden + b)[:width]
315
+ mx.eval(logits)
316
+ values = np.asarray(logits)
317
+ if not np.isfinite(values).all():
318
+ raise ValueError("Nonfinite trained-head output")
319
+ result = {
320
+ "letter_logits": values.tolist(),
321
+ "head_key": head,
322
+ "prompt_tokens": len(ids),
323
+ "branch_tokens": len(ids) - p,
324
+ "reused_prefix_tokens": p if execution == "cached" else 0,
325
+ "seconds": time.perf_counter() - started,
326
+ }
327
+ if taps:
328
+ result.update(
329
+ hidden=np.asarray(hidden).tolist(),
330
+ taps={k: np.asarray(v).tolist() for k, v in captured.items()},
331
+ token_ids=ids,
332
+ )
333
+ return result
mlx/src/solomon_mlx/evaluation.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resumable panel scoring, fit-only calibration and one-shot held-out reports."""
2
+
3
+ import gzip
4
+ import hashlib
5
+ import json
6
+ from collections import defaultdict
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ from scipy.optimize import minimize_scalar
11
+
12
+ from ._vendor.semantics import listed_probs, p_yes
13
+ from .api import TASKS
14
+ from .artifacts import ADAPTER_SHA, HEADS_SHA, digest, sha256
15
+
16
+
17
+ def load_panel(directory):
18
+ directory = Path(directory)
19
+ manifest = json.loads((directory / "manifest.json").read_text())
20
+ if (
21
+ manifest["adapter_sha256"] != ADAPTER_SHA
22
+ or manifest["heads_sha256"] != HEADS_SHA
23
+ or manifest["readout_mode"] != "four_collapsed"
24
+ ):
25
+ raise ValueError("Panel belongs to a different checkpoint or answer semantics")
26
+ raw = gzip.decompress((directory / "jobs.json.gz").read_bytes())
27
+ if hashlib.sha256(raw).hexdigest() != manifest["jobs_sha256"]:
28
+ raise ValueError("Panel jobs checksum mismatch")
29
+ jobs = json.loads(raw)
30
+ if len({r["id"] for r in jobs}) != len(jobs):
31
+ raise ValueError("Duplicate panel branch IDs")
32
+ return jobs, manifest
33
+
34
+
35
+ def score_panel(model, panel, output):
36
+ """Atomically persist each document so interruption never requires rescoring it."""
37
+ jobs, manifest = load_panel(panel)
38
+ output = Path(output)
39
+ output.mkdir(parents=True, exist_ok=True)
40
+ identity = {
41
+ "runtime": model.identity,
42
+ "panel_sha256": manifest["jobs_sha256"],
43
+ "panel_role": Path(panel).name.split("-")[0],
44
+ }
45
+ meta = output / "identity.json"
46
+ if meta.exists() and json.loads(meta.read_text()) != identity:
47
+ raise ValueError("Cannot resume with different model code, weights or panel")
48
+ meta.write_text(json.dumps(identity, indent=2))
49
+ documents = defaultdict(list)
50
+ for row in jobs:
51
+ documents[row["document_key"]].append(row)
52
+ for key, group in documents.items():
53
+ path = output / (key + ".json")
54
+ if path.exists():
55
+ record = json.loads(path.read_text())
56
+ body = {k: v for k, v in record.items() if k != "sha256"}
57
+ if (
58
+ record["sha256"] != digest(body)
59
+ or record["identity"] != digest(identity)
60
+ or [r["id"] for r in record["rows"]] != [r["id"] for r in group]
61
+ ):
62
+ raise ValueError("Corrupt or mismatched resumed document")
63
+ continue
64
+ parts = group[0].get("parts") or [{"text": group[0]["doc"]}]
65
+ if any((r.get("parts") or [{"text": r["doc"]}]) != parts for r in group):
66
+ raise ValueError("Document key aliases different sources")
67
+ with model.prefill(parts) as state:
68
+ rows = []
69
+ for job in group:
70
+ result = model.engine.ask(state._data, job["block"], job["n"], job["head_key"])
71
+ rows.append({**result, **{k: job[k] for k in ("id", "task", "gold", "n", "question_id")}})
72
+ body = {
73
+ "identity": digest(identity),
74
+ "rows": rows,
75
+ "prefix_tokens": state.prefix_tokens,
76
+ "prefill_seconds": state._data["prefill_seconds"],
77
+ }
78
+ temp = path.with_suffix(".tmp")
79
+ temp.write_text(json.dumps({**body, "sha256": digest(body)}))
80
+ temp.replace(path)
81
+ print("Scored " + key + " " + str(len(rows)) + " branches", flush=True)
82
+ completed = {
83
+ "identity": digest(identity),
84
+ "documents": len(documents),
85
+ "branches": len(jobs),
86
+ "files": {key + ".json": sha256(output / (key + ".json")) for key in documents},
87
+ }
88
+ (output / "complete.json").write_text(json.dumps(completed, indent=2))
89
+
90
+
91
+ def read_scores(directory):
92
+ directory = Path(directory)
93
+ identity = json.loads((directory / "identity.json").read_text())
94
+ completed = json.loads((directory / "complete.json").read_text())
95
+ if completed["identity"] != digest(identity):
96
+ raise ValueError("Score identity mismatch")
97
+ rows = []
98
+ for name, checksum in completed["files"].items():
99
+ p = directory / name
100
+ if not p.resolve().is_relative_to(directory.resolve()) or sha256(p) != checksum:
101
+ raise ValueError("Score checksum mismatch")
102
+ record = json.loads(p.read_text())
103
+ rows.extend(record["rows"])
104
+ if len(rows) != completed["branches"]:
105
+ raise ValueError("Incomplete score set")
106
+ return rows, identity
107
+
108
+
109
+ def unit(row, temperature=1.0):
110
+ logits = row["letter_logits"]
111
+ task = row["task"]
112
+ gold = row["gold"]
113
+ if task in ("boolean", "entity", "multilabel"):
114
+ p = p_yes(logits, temperature)
115
+ return [1 - p, p], int(gold == 0)
116
+ width = row["n"] - 2 if row["head_key"].endswith("choiceR") else row["n"]
117
+ if not isinstance(gold, int) or not 0 <= gold < width:
118
+ return None, None
119
+ return listed_probs(logits, width, temperature).tolist(), gold
120
+
121
+
122
+ def fit_calibration(scores, output, *, panel_role):
123
+ if panel_role != "fit":
124
+ raise ValueError("Temperature fitting accepts fit panels only")
125
+ rows, identity = read_scores(scores)
126
+ if identity["panel_role"] != "fit":
127
+ raise ValueError("Scores were not generated from a fit panel")
128
+ output = Path(output)
129
+ if output.exists():
130
+ raise FileExistsError("Calibration artifacts are immutable")
131
+ temperatures, losses = {}, {}
132
+ for task in TASKS:
133
+ selected = [r for r in rows if r["task"] == task and unit(r)[0] is not None]
134
+ if not selected:
135
+ raise ValueError("No fit examples for " + task)
136
+
137
+ def loss(log_t, selected=selected):
138
+ t = float(np.exp(log_t))
139
+ return float(np.mean([-np.log(max(unit(r, t)[0][unit(r, t)[1]], 1e-300)) for r in selected]))
140
+
141
+ fit = minimize_scalar(loss, bounds=(np.log(0.05), np.log(20)), method="bounded")
142
+ temperatures[task] = float(np.exp(fit.x))
143
+ losses[task] = {"before": loss(0.0), "after": float(fit.fun), "units": len(selected)}
144
+ payload = {
145
+ "schema": "solomon-mlx-temperature-v1",
146
+ "runtime": identity["runtime"]["fingerprint"],
147
+ "temperatures": temperatures,
148
+ "fit_panel_sha256": identity["panel_sha256"],
149
+ "losses": losses,
150
+ "selection_role": "fit",
151
+ "heldout_used": False,
152
+ }
153
+ output.write_text(json.dumps({**payload, "sha256": digest(payload)}, indent=2))
154
+ return payload
155
+
156
+
157
+ def compare_rows(mlx_rows, cuda_rows, *, temperatures=None, reference_temperatures=None):
158
+ temperatures = temperatures or dict.fromkeys(TASKS, 1.0)
159
+ reference_temperatures = reference_temperatures or dict.fromkeys(TASKS, 1.0)
160
+ reference = {r["id"]: r for r in cuda_rows}
161
+ if len(reference) != len(cuda_rows) or set(reference) != {r["id"] for r in mlx_rows}:
162
+ raise ValueError("Comparison panels have different or duplicate branch IDs")
163
+ units = []
164
+ questions = defaultdict(list)
165
+ for row in mlx_rows:
166
+ other = {**row, "letter_logits": reference[row["id"]]["letter_logits"]}
167
+ p, gold = unit(row, temperatures[row["task"]])
168
+ q, _ = unit(other, reference_temperatures[row["task"]])
169
+ if p is None:
170
+ continue
171
+ left, right = int(np.argmax(p)), int(np.argmax(q))
172
+ item = {
173
+ "agreement": left == right,
174
+ "mlx_correct": left == gold,
175
+ "cuda_correct": right == gold,
176
+ "probability_drift": float(np.max(np.abs(np.asarray(p) - q))),
177
+ }
178
+ units.append(item)
179
+ questions[row["question_id"]].append(item)
180
+ if not units:
181
+ raise ValueError("No defined comparison targets")
182
+ agreement = float(np.mean([r["agreement"] for r in units]))
183
+ question_agreement = float(np.mean([all(x["agreement"] for x in r) for r in questions.values()]))
184
+ mlx_accuracy = float(np.mean([all(x["mlx_correct"] for x in r) for r in questions.values()]))
185
+ cuda_accuracy = float(np.mean([all(x["cuda_correct"] for x in r) for r in questions.values()]))
186
+ return {
187
+ "units": len(units),
188
+ "questions": len(questions),
189
+ "unit_decision_agreement": agreement,
190
+ "question_decision_agreement": float(
191
+ np.mean([all(x["agreement"] for x in r) for r in questions.values()])
192
+ ),
193
+ "mlx_whole_question_accuracy": mlx_accuracy,
194
+ "cuda_whole_question_accuracy": cuda_accuracy,
195
+ "accuracy_degradation_percentage_points": 100 * (cuda_accuracy - mlx_accuracy),
196
+ "max_probability_drift": max(r["probability_drift"] for r in units),
197
+ "probability_comparison": {
198
+ "mlx_temperatures": temperatures,
199
+ "cuda_temperatures": reference_temperatures,
200
+ },
201
+ "mean_probability_drift": float(np.mean([r["probability_drift"] for r in units])),
202
+ "quality_gate_passed": agreement >= 0.999
203
+ and question_agreement >= 0.999
204
+ and cuda_accuracy - mlx_accuracy <= 0.0025,
205
+ }
206
+
207
+
208
+ def read_cuda_scores(directory, panel, reference_identity):
209
+ """Reuse only scores bound to the exact pinned CUDA runtime and panel."""
210
+ jobs, manifest = load_panel(panel)
211
+ directory = Path(directory)
212
+ result = {}
213
+ for file in sorted(directory.glob("scores*.json.gz")):
214
+ payload = json.loads(gzip.decompress(file.read_bytes()))
215
+ identity = payload["identity"]
216
+ if not payload["complete"] or identity["runtime"] != reference_identity:
217
+ raise ValueError("Existing CUDA scores do not match the fresh reference runtime")
218
+ if identity["manifest"]["jobs_sha256"] != manifest["jobs_sha256"]:
219
+ raise ValueError("CUDA scores use another panel")
220
+ for key, row in payload["scores"].items():
221
+ if key in result:
222
+ raise ValueError("Duplicate CUDA score ID")
223
+ result[key] = row
224
+ if set(result) != {r["id"] for r in jobs}:
225
+ raise ValueError("CUDA score set is incomplete")
226
+ return [{**r, **result[r["id"]]} for r in jobs]
227
+
228
+
229
+ def select_calibration(fitted, dev_scores, output):
230
+ fitted, output = Path(fitted), Path(output)
231
+ if output.exists():
232
+ raise FileExistsError("Selected calibration is immutable")
233
+ fit = json.loads(fitted.read_text())
234
+ fit_payload = {k: v for k, v in fit.items() if k != "sha256"}
235
+ rows, identity = read_scores(dev_scores)
236
+ if (
237
+ fit["sha256"] != digest(fit_payload)
238
+ or identity["runtime"]["fingerprint"] != fit["runtime"]
239
+ or identity["panel_role"] != "dev"
240
+ ):
241
+ raise ValueError("Calibration or development identity mismatch")
242
+ temperatures, selection = {}, {}
243
+ for task in TASKS:
244
+ selected = [r for r in rows if r["task"] == task and unit(r)[0] is not None]
245
+ if not selected:
246
+ raise ValueError("Missing development task " + task)
247
+
248
+ def loss(t, selected=selected):
249
+ values = [unit(r, t) for r in selected]
250
+ return float(np.mean([-np.log(max(p[g], 1e-300)) for p, g in values]))
251
+
252
+ original, candidate = loss(1.0), loss(fit["temperatures"][task])
253
+ temperatures[task] = fit["temperatures"][task] if candidate < original else 1.0
254
+ selection[task] = {"untempered_nll": original, "fit_temperature_nll": candidate}
255
+ payload = {
256
+ **fit_payload,
257
+ "temperatures": temperatures,
258
+ "selection_role": "dev_selected",
259
+ "fit_artifact_sha256": sha256(fitted),
260
+ "dev_panel_sha256": identity["panel_sha256"],
261
+ "development_selection": selection,
262
+ }
263
+ output.write_text(json.dumps({**payload, "sha256": digest(payload)}, indent=2))
264
+ return payload
265
+
266
+
267
+ def heldout_report(
268
+ scores,
269
+ cuda_directory,
270
+ panel,
271
+ calibration,
272
+ reference,
273
+ output,
274
+ *,
275
+ reference_binding="evaluations/cuda-acceptance/input/serving-binding.json",
276
+ ):
277
+ """Evaluate a frozen configuration once; an existing output cannot be replaced."""
278
+ output = Path(output)
279
+ if output.exists():
280
+ raise FileExistsError("Held-out report already exists; do not reuse it for selection")
281
+ rows, identity = read_scores(scores)
282
+ cal = json.loads(Path(calibration).read_text())
283
+ payload = {k: v for k, v in cal.items() if k != "sha256"}
284
+ if (
285
+ cal["sha256"] != digest(payload)
286
+ or cal["runtime"] != identity["runtime"]["fingerprint"]
287
+ or cal["selection_role"] != "dev_selected"
288
+ or identity["panel_role"] != "cert"
289
+ ):
290
+ raise ValueError(
291
+ "Held-out evaluation requires frozen development-selected calibration and cert scores"
292
+ )
293
+ ref = json.loads(Path(reference).read_text())
294
+ cuda = read_cuda_scores(cuda_directory, panel, ref["identity"])
295
+ binding_path = Path(reference_binding)
296
+ source_manifest = json.loads((binding_path.parent / "manifest.json").read_text())
297
+ if sha256(binding_path) != source_manifest["files"][binding_path.name]:
298
+ raise ValueError("CUDA acceptance binding checksum mismatch")
299
+ binding = json.loads(binding_path.read_text())
300
+ for key in (
301
+ "adapter_sha256",
302
+ "trained_heads_sha256",
303
+ "model_sha256",
304
+ "numerics",
305
+ "placement",
306
+ "arithmetic",
307
+ ):
308
+ if binding["runtime"][key] != ref["identity"][key]:
309
+ raise ValueError("CUDA calibration belongs to another reference runtime")
310
+ reference_temperatures = {task: binding["temperatures"]["models"][task]["temperature"] for task in TASKS}
311
+ report = {
312
+ **compare_rows(
313
+ rows, cuda, temperatures=cal["temperatures"], reference_temperatures=reference_temperatures
314
+ ),
315
+ "cuda_calibration_binding_sha256": sha256(binding_path),
316
+ "runtime": identity["runtime"],
317
+ "panel_sha256": identity["panel_sha256"],
318
+ "calibration_sha256": sha256(calibration),
319
+ "reference_sha256": sha256(reference),
320
+ "scope": "text-only held-out panel",
321
+ "image_qualification": False,
322
+ }
323
+ output.write_text(json.dumps(report, indent=2))
324
+ return report
mlx/src/solomon_mlx/prepare.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify immutable input snapshots and create a separately checksummed BF16 model."""
2
+
3
+ import hashlib
4
+ import json
5
+ import shutil
6
+ from importlib.metadata import version
7
+ from pathlib import Path
8
+
9
+ from .artifacts import BASE_REVISION, SOLOMON_REVISION, sha256, verify_release
10
+
11
+
12
+ def verify_base(root, manifest):
13
+ root = Path(root)
14
+ if manifest["revision"] != BASE_REVISION:
15
+ raise ValueError("Wrong pinned base revision")
16
+ result = {}
17
+ for row in manifest["files"]:
18
+ path = root / row["name"]
19
+ if not path.resolve().is_relative_to(root.resolve()):
20
+ raise ValueError("Unsafe base manifest path")
21
+ if path.stat().st_size != row["size"]:
22
+ raise ValueError("Base size mismatch: " + row["name"])
23
+ actual = sha256(path)
24
+ if row["sha256"]:
25
+ if actual != row["sha256"]:
26
+ raise ValueError("Base checksum mismatch: " + row["name"])
27
+ else:
28
+ data = path.read_bytes()
29
+ git_hash = hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest()
30
+ if git_hash != row["blob_id"]:
31
+ raise ValueError("Base Git blob mismatch: " + row["name"])
32
+ result[row["name"]] = actual
33
+ return result
34
+
35
+
36
+ def prepare(base, solomon, output, manifest):
37
+
38
+ base, solomon, output = map(Path, (base, solomon, output))
39
+ source_hashes = verify_release(solomon)
40
+ base_hashes = verify_base(base, json.loads(Path(manifest).read_text()))
41
+ if output.exists():
42
+ raise FileExistsError("Use a new output directory; existing conversions are immutable")
43
+ output.mkdir(parents=True)
44
+ convert_bf16(base, output / "backbone")
45
+ shutil.copy2(solomon / "adapter/adapter.safetensors", output / "adapter.safetensors")
46
+ shutil.copy2(solomon / "adapter/heads.npz", output / "heads.npz")
47
+ for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md"):
48
+ shutil.copy2(solomon / name, output / name)
49
+ files = {
50
+ str(p.relative_to(output)): sha256(p)
51
+ for p in sorted(output.rglob("*"))
52
+ if p.is_file() and ".cache" not in p.parts
53
+ }
54
+ binding = {
55
+ "schema": "solomon-mlx-binding-v1",
56
+ "profile": "quality",
57
+ "dtype": "bfloat16",
58
+ "adapter_dtype": "float32",
59
+ "head_dtype": "float32",
60
+ "recurrent_state_dtype": "float32",
61
+ "sensitive_parameters": "FP32 normalization weights, A_log and dt_bias",
62
+ "conversion": "upstream Qwen3.5 sanitization, norms promoted before unit offset",
63
+ "adapter_scale": 2.0,
64
+ "adapter_placement": "question",
65
+ "quantization": None,
66
+ "base_revision": BASE_REVISION,
67
+ "solomon_revision": SOLOMON_REVISION,
68
+ "source_hashes": source_hashes,
69
+ "base_hashes": base_hashes,
70
+ "dependencies": {p: version(p) for p in ("mlx", "mlx-vlm", "transformers", "numpy")},
71
+ "files": files,
72
+ }
73
+ (output / "binding.json").write_text(json.dumps(binding, indent=2, sort_keys=True))
74
+ return binding
75
+
76
+
77
+ def convert_bf16(base, output):
78
+ """Convert one original shard at a time; never copy download caches.
79
+
80
+ Keep normalization offsets in FP32 before adding one. Adding the unit
81
+ offset in BF16 would irreversibly round trained normalization parameters.
82
+ Large backbone matrices remain unquantized BF16.
83
+ """
84
+ import mlx.core as mx
85
+ from mlx.utils import tree_flatten
86
+ from mlx_vlm.models.qwen3_5.config import ModelConfig
87
+ from mlx_vlm.models.qwen3_5.qwen3_5 import Model
88
+
89
+ base, output = Path(base), Path(output)
90
+ output.mkdir(parents=True, exist_ok=False)
91
+ config = json.loads((base / "config.json").read_text())
92
+ if config.get("model_type") != "qwen3_5" or config.get("quantization"):
93
+ raise ValueError("Expected original unquantized Qwen3.5 architecture")
94
+ model = Model(ModelConfig.from_dict(config))
95
+ expected = {k: v.shape for k, v in tree_flatten(model.parameters())}
96
+ index = {"metadata": {"total_size": 0}, "weight_map": {}}
97
+ source_index = json.loads((base / "model.safetensors.index.json").read_text())
98
+ for filename in sorted(set(source_index["weight_map"].values())):
99
+ arrays = mx.load(str(base / filename))
100
+ for key, value in arrays.items():
101
+ sensitive = value.ndim == 1 and ("norm" in key or key.endswith(("A_log", "dt_bias")))
102
+ arrays[key] = value.astype(mx.float32 if sensitive else mx.bfloat16)
103
+ arrays = model.sanitize(arrays)
104
+ arrays = model.vision_tower.sanitize(arrays)
105
+ for key, value in arrays.items():
106
+ if key not in expected or value.shape != expected[key]:
107
+ raise ValueError("Converted tensor shape mismatch: " + key)
108
+ if key in index["weight_map"]:
109
+ raise ValueError("Duplicate converted tensor: " + key)
110
+ index["weight_map"][key] = filename
111
+ index["metadata"]["total_size"] += value.nbytes
112
+ mx.save_safetensors(str(output / filename), arrays, metadata={"format": "mlx"})
113
+ del arrays
114
+ mx.clear_cache()
115
+ print("Converted " + filename, flush=True)
116
+ if set(index["weight_map"]) != set(expected):
117
+ raise ValueError("Converted backbone is missing required parameters")
118
+ (output / "model.safetensors.index.json").write_text(json.dumps(index, indent=2, sort_keys=True))
119
+ for file in base.iterdir():
120
+ if file.is_file() and file.name != "model.safetensors.index.json" and file.suffix != ".safetensors":
121
+ shutil.copy2(file, output / file.name)
mlx/src/solomon_mlx_hub/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Prepare the adapter-only Hub release for the frozen Solomon MLX runtime."""
2
+
3
+ from .prepare import load, prepare_from_hub, prepare_from_snapshot, verify_prepared
4
+
5
+ __all__ = ["load", "prepare_from_hub", "prepare_from_snapshot", "verify_prepared"]
mlx/src/solomon_mlx_hub/__main__.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adapter-only Hub setup and verification command."""
2
+
3
+ import argparse
4
+ import json
5
+
6
+ from .prepare import prepare_from_hub, prepare_from_snapshot, verify_prepared
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description="Prepare full BF16 Solomon from the adapter-only release")
11
+ commands = parser.add_subparsers(dest="command", required=True)
12
+ prepare = commands.add_parser("prepare")
13
+ prepare.add_argument("--revision", required=True, help="Full Solomon repository commit SHA")
14
+ prepare.add_argument("--output", default="models/quality")
15
+ prepare.add_argument("--base", help="Existing original Qwen snapshot, already downloaded")
16
+ prepare.add_argument("--snapshot", help="Existing adapter-only Solomon snapshot; requires --base")
17
+ prepare.add_argument("--cache-dir")
18
+ prepare.add_argument("--device", choices=("cpu", "gpu"), default="cpu")
19
+ verify = commands.add_parser("verify")
20
+ verify.add_argument("directory")
21
+ args = parser.parse_args()
22
+ if args.command == "verify":
23
+ binding = verify_prepared(args.directory)
24
+ print(json.dumps({"verified": True, "files": len(binding["files"]), "profile": binding["profile"]}))
25
+ elif args.snapshot:
26
+ if not args.base:
27
+ parser.error("--snapshot requires --base for offline preparation")
28
+ print(
29
+ prepare_from_snapshot(
30
+ args.snapshot,
31
+ args.output,
32
+ revision=args.revision,
33
+ base_dir=args.base,
34
+ device=args.device,
35
+ )
36
+ )
37
+ else:
38
+ print(
39
+ prepare_from_hub(
40
+ args.output,
41
+ revision=args.revision,
42
+ base_dir=args.base,
43
+ cache_dir=args.cache_dir,
44
+ device=args.device,
45
+ )
46
+ )
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
mlx/src/solomon_mlx_hub/data/base-manifest.json ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "revision": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
3
+ "files": [
4
+ {
5
+ "name": ".gitattributes",
6
+ "size": 1570,
7
+ "blob_id": "52373fe24473b1aa44333d318f578ae6bf04b49b",
8
+ "sha256": null
9
+ },
10
+ {
11
+ "name": "LICENSE",
12
+ "size": 11544,
13
+ "blob_id": "f938136e3adacfd92be087f6e113b5d6d97f678f",
14
+ "sha256": null
15
+ },
16
+ {
17
+ "name": "README.md",
18
+ "size": 65012,
19
+ "blob_id": "bc8aa0e396cd029c21cd773cca21830d0ded28ec",
20
+ "sha256": null
21
+ },
22
+ {
23
+ "name": "chat_template.jinja",
24
+ "size": 8952,
25
+ "blob_id": "c0c686f9c38d70d179fb7b5f5aa7530bc913dda3",
26
+ "sha256": null
27
+ },
28
+ {
29
+ "name": "config.json",
30
+ "size": 4312,
31
+ "blob_id": "706cebd746c4b6f2b1d1f892630867acfdfd3df8",
32
+ "sha256": null
33
+ },
34
+ {
35
+ "name": "crc32.txt",
36
+ "size": 238,
37
+ "blob_id": "6de5ee6a0c6596744baee911af0a5cdcb8d99a1e",
38
+ "sha256": null
39
+ },
40
+ {
41
+ "name": "generation_config.json",
42
+ "size": 202,
43
+ "blob_id": "023756cfadf88e5bf69eefeee3e172f38c448d64",
44
+ "sha256": null
45
+ },
46
+ {
47
+ "name": "merges.txt",
48
+ "size": 3353259,
49
+ "blob_id": "a494e019ca1502219fd0128658b979e5f05ae8e8",
50
+ "sha256": null
51
+ },
52
+ {
53
+ "name": "model-00001-of-00018.safetensors",
54
+ "size": 3966730552,
55
+ "blob_id": "79b73cdd69afeca931d181f5d1e21ad2d73bb7b7",
56
+ "sha256": "ba0ce20aae489ad196733da5064bcdf159a1fe84f53336648196e1ebb7751b1c"
57
+ },
58
+ {
59
+ "name": "model-00002-of-00018.safetensors",
60
+ "size": 3043080328,
61
+ "blob_id": "81cf43545884a227a01351942dfbc72577812316",
62
+ "sha256": "06a148c01bfbe3faa14a5f184a7ff29a706f7ae1c8b2705d2058e26d17a001fb"
63
+ },
64
+ {
65
+ "name": "model-00003-of-00018.safetensors",
66
+ "size": 2542796952,
67
+ "blob_id": "a2da5e6cd6307a6cae13d04820ab0d31b47b48bd",
68
+ "sha256": "2e1bf62cbcd406eaa64b60d10353e1f0ef4039d0976e56f05cabe953454f9968"
69
+ },
70
+ {
71
+ "name": "model-00004-of-00018.safetensors",
72
+ "size": 3988973152,
73
+ "blob_id": "175ddab3f2488ad181f73d789d2d778cf6d4fca6",
74
+ "sha256": "511e34063187882659753c4d93f3859f93c019fd438d8813071921c81d9a3f1a"
75
+ },
76
+ {
77
+ "name": "model-00005-of-00018.safetensors",
78
+ "size": 2099339864,
79
+ "blob_id": "64890208fdd563c3aac0c2f55f69948bcaa1a492",
80
+ "sha256": "635cb53446dc74f219740fc59e18b774f877b803b9722e289ca62575a6efa701"
81
+ },
82
+ {
83
+ "name": "model-00006-of-00018.safetensors",
84
+ "size": 3979553696,
85
+ "blob_id": "3ae29b86e4c773babfe17124dbd10d54d37819ec",
86
+ "sha256": "0bc5214fac607f0e6cc92eec3789d4b8559410ef9fce66621ba8158e8410dae0"
87
+ },
88
+ {
89
+ "name": "model-00007-of-00018.safetensors",
90
+ "size": 2108759344,
91
+ "blob_id": "b7e9c0f038884839f3dad38fef7e75ee3362c24f",
92
+ "sha256": "80b0c49033e9a0d5762562aa12f4acdb7f54da586f3d0110f28c48d91cf07892"
93
+ },
94
+ {
95
+ "name": "model-00008-of-00018.safetensors",
96
+ "size": 3979553696,
97
+ "blob_id": "0b58cbc52714e1c683711be87e7c2e33fb10e7a0",
98
+ "sha256": "7192c5b66185d3592927daabee1cc19e6f6e0ce75988ee20e824b624765fda79"
99
+ },
100
+ {
101
+ "name": "model-00009-of-00018.safetensors",
102
+ "size": 2108759344,
103
+ "blob_id": "4fc0c6b28004d809225d0e115f30e9e11c2345dc",
104
+ "sha256": "af3c48cc37af44f3db6ae0579baf019180d48d9c527caa0a1f03ff85813a56d8"
105
+ },
106
+ {
107
+ "name": "model-00010-of-00018.safetensors",
108
+ "size": 3979553696,
109
+ "blob_id": "9ec693a17c788084116c51b4f34632670df77612",
110
+ "sha256": "163490a76f3bea3a40855b7efc04ce6d27afaf1a34f0bbde495b9491f76457c9"
111
+ },
112
+ {
113
+ "name": "model-00011-of-00018.safetensors",
114
+ "size": 2108759344,
115
+ "blob_id": "3c63baaed16d833432a919647af53ac76bb6b007",
116
+ "sha256": "5f3ae1b948aeee39da77aec558e8236cd65fe4d7cb7686a76bb007acc563c6d8"
117
+ },
118
+ {
119
+ "name": "model-00012-of-00018.safetensors",
120
+ "size": 3979553696,
121
+ "blob_id": "8568b440a4666ed1b4cda0b5d21496378304f326",
122
+ "sha256": "a3de1c7114677a8f5ac5c4892c90e8238ea5c1e2038c80e757dfc87c3902ca55"
123
+ },
124
+ {
125
+ "name": "model-00013-of-00018.safetensors",
126
+ "size": 2108759344,
127
+ "blob_id": "724083995a2257194321a3d00b36fd7d4bb7348d",
128
+ "sha256": "06ab79a41f74c9c5cb734816feb0c7fc364104b227165ee7391231e1155aa02a"
129
+ },
130
+ {
131
+ "name": "model-00014-of-00018.safetensors",
132
+ "size": 3979553696,
133
+ "blob_id": "a60b80b6510d7938d951fbddc76aaadd121c6268",
134
+ "sha256": "4138ed94603065ba884bbcadedb04d7718bb40117e85e6f5c6fc5b9c05b7a85b"
135
+ },
136
+ {
137
+ "name": "model-00015-of-00018.safetensors",
138
+ "size": 2108759344,
139
+ "blob_id": "7d4c34f3d716e1a47acff2f576b65b41a3ec4f3f",
140
+ "sha256": "69224e27b9de4e7dbf6fc936c6eaae08447bda3b80a6c31a871ab451173afd22"
141
+ },
142
+ {
143
+ "name": "model-00016-of-00018.safetensors",
144
+ "size": 3979564040,
145
+ "blob_id": "c67e3e42c9715298fa7f68c6b378bfb1d74290d0",
146
+ "sha256": "73cb9a1089fb6155cb648609478d6633be8a5c7d9ca5a05bc8925ce8a553cefe"
147
+ },
148
+ {
149
+ "name": "model-00017-of-00018.safetensors",
150
+ "size": 2108759344,
151
+ "blob_id": "23dd899179729a4370c655e2c2241cfdbe4ef4ba",
152
+ "sha256": "beb51f01056142ac4984bd800507b0dd0fd18de57f8e9ef6ea41d1a3598983a8"
153
+ },
154
+ {
155
+ "name": "model-00018-of-00018.safetensors",
156
+ "size": 3392197344,
157
+ "blob_id": "a92b4073fc6a58ffc9334ac63360fc8d9cc50d33",
158
+ "sha256": "1d3479509e21494658f9b64d317f5ea8e55c4025d28c702d6c4d0b356ce8ea06"
159
+ },
160
+ {
161
+ "name": "model.safetensors.index.json",
162
+ "size": 112216,
163
+ "blob_id": "da35e3c564457dface7d138f0b6cac284ff8958c",
164
+ "sha256": null
165
+ },
166
+ {
167
+ "name": "preprocessor_config.json",
168
+ "size": 390,
169
+ "blob_id": "2ea84a437d448ff71b08df68fdd949d5cc4ebb64",
170
+ "sha256": null
171
+ },
172
+ {
173
+ "name": "tokenizer.json",
174
+ "size": 12809320,
175
+ "blob_id": "9328ce9c41e80f6dd7bc2c66d8ae1fc93bf87440",
176
+ "sha256": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3"
177
+ },
178
+ {
179
+ "name": "tokenizer_config.json",
180
+ "size": 17928,
181
+ "blob_id": "5de744b3fca2129d7186979ae47c06be33903243",
182
+ "sha256": null
183
+ },
184
+ {
185
+ "name": "video_preprocessor_config.json",
186
+ "size": 385,
187
+ "blob_id": "3ba673a5ad7d4d13f54155ecd38b2a94a6dac8fe",
188
+ "sha256": null
189
+ },
190
+ {
191
+ "name": "vocab.json",
192
+ "size": 6722759,
193
+ "blob_id": "0aa0ce0658d60ac4a5d609f4eadb0e8e43514176",
194
+ "sha256": null
195
+ }
196
+ ]
197
+ }
mlx/src/solomon_mlx_hub/data/release.json ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "base_manifest_sha256": "e62a6942465b43f7f47616e36a16d6f32a1bd9daa5124dd26bf61b8eab0c3b6d",
3
+ "base_repo_id": "Qwen/Qwen3.8-27B",
4
+ "binding_contract": {
5
+ "adapter_dtype": "float32",
6
+ "adapter_placement": "question",
7
+ "adapter_scale": 2.0,
8
+ "base_hashes": {
9
+ ".gitattributes": "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930",
10
+ "LICENSE": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a",
11
+ "README.md": "57e4bdb258ee1a7d2635c5174ebd4e56abe392505cdb5f8bbb356b0dc4293641",
12
+ "chat_template.jinja": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041",
13
+ "config.json": "191e0af232104ed8b65258cf3fb2b842e288008baca7633c11b82a1ac7203aab",
14
+ "crc32.txt": "b42dd291f5f98b05807e458f5b47849969a9b3326acbf0c97d28b05826740b83",
15
+ "generation_config.json": "e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e",
16
+ "merges.txt": "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d",
17
+ "model-00001-of-00018.safetensors": "ba0ce20aae489ad196733da5064bcdf159a1fe84f53336648196e1ebb7751b1c",
18
+ "model-00002-of-00018.safetensors": "06a148c01bfbe3faa14a5f184a7ff29a706f7ae1c8b2705d2058e26d17a001fb",
19
+ "model-00003-of-00018.safetensors": "2e1bf62cbcd406eaa64b60d10353e1f0ef4039d0976e56f05cabe953454f9968",
20
+ "model-00004-of-00018.safetensors": "511e34063187882659753c4d93f3859f93c019fd438d8813071921c81d9a3f1a",
21
+ "model-00005-of-00018.safetensors": "635cb53446dc74f219740fc59e18b774f877b803b9722e289ca62575a6efa701",
22
+ "model-00006-of-00018.safetensors": "0bc5214fac607f0e6cc92eec3789d4b8559410ef9fce66621ba8158e8410dae0",
23
+ "model-00007-of-00018.safetensors": "80b0c49033e9a0d5762562aa12f4acdb7f54da586f3d0110f28c48d91cf07892",
24
+ "model-00008-of-00018.safetensors": "7192c5b66185d3592927daabee1cc19e6f6e0ce75988ee20e824b624765fda79",
25
+ "model-00009-of-00018.safetensors": "af3c48cc37af44f3db6ae0579baf019180d48d9c527caa0a1f03ff85813a56d8",
26
+ "model-00010-of-00018.safetensors": "163490a76f3bea3a40855b7efc04ce6d27afaf1a34f0bbde495b9491f76457c9",
27
+ "model-00011-of-00018.safetensors": "5f3ae1b948aeee39da77aec558e8236cd65fe4d7cb7686a76bb007acc563c6d8",
28
+ "model-00012-of-00018.safetensors": "a3de1c7114677a8f5ac5c4892c90e8238ea5c1e2038c80e757dfc87c3902ca55",
29
+ "model-00013-of-00018.safetensors": "06ab79a41f74c9c5cb734816feb0c7fc364104b227165ee7391231e1155aa02a",
30
+ "model-00014-of-00018.safetensors": "4138ed94603065ba884bbcadedb04d7718bb40117e85e6f5c6fc5b9c05b7a85b",
31
+ "model-00015-of-00018.safetensors": "69224e27b9de4e7dbf6fc936c6eaae08447bda3b80a6c31a871ab451173afd22",
32
+ "model-00016-of-00018.safetensors": "73cb9a1089fb6155cb648609478d6633be8a5c7d9ca5a05bc8925ce8a553cefe",
33
+ "model-00017-of-00018.safetensors": "beb51f01056142ac4984bd800507b0dd0fd18de57f8e9ef6ea41d1a3598983a8",
34
+ "model-00018-of-00018.safetensors": "1d3479509e21494658f9b64d317f5ea8e55c4025d28c702d6c4d0b356ce8ea06",
35
+ "model.safetensors.index.json": "77042094076611b69791a610065f28b7013b8c621795fa86ddccc8bac7d1b9df",
36
+ "preprocessor_config.json": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516",
37
+ "tokenizer.json": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3",
38
+ "tokenizer_config.json": "b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27",
39
+ "video_preprocessor_config.json": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13",
40
+ "vocab.json": "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003"
41
+ },
42
+ "base_revision": "1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0",
43
+ "conversion": "upstream Qwen3.5 sanitization, norms promoted before unit offset",
44
+ "dependencies": {
45
+ "mlx": "0.32.2",
46
+ "mlx-vlm": "0.7.1",
47
+ "numpy": "2.5.3",
48
+ "transformers": "5.17.0"
49
+ },
50
+ "dtype": "bfloat16",
51
+ "head_dtype": "float32",
52
+ "profile": "quality",
53
+ "quantization": null,
54
+ "recurrent_state_dtype": "float32",
55
+ "schema": "solomon-mlx-binding-v1",
56
+ "sensitive_parameters": "FP32 normalization weights, A_log and dt_bias",
57
+ "solomon_revision": "2ec506902269e4636285c8811f6c0f52c9300c0c"
58
+ },
59
+ "converter_code_sha256": "bbcae17fc1c35db80a79d5865133a42ef9a1b0cf342fff71949972e69cce43ec",
60
+ "reference_files": {
61
+ "LICENSE": "a61385b8bc4e786195136404b4975fede3c0bca526877f526482e9af93d47457",
62
+ "MODIFICATIONS.md": "218218b43b27132a6cfa032d2be908decd0966f5b655946a8668506731c77de7",
63
+ "NOTICE": "ccf2b8e98f0606945a5fcf9f6522c1f2c01b279d55dc21ee02c5a4acadf8e8d1",
64
+ "adapter.safetensors": "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca",
65
+ "backbone/.gitattributes": "34448b82c17d60fec9b65b1f093c115ddbaadc04beb1b0140b6bfed2e012a930",
66
+ "backbone/LICENSE": "bbedc3fda3305820b977265f01b8619d87570a6739de3a5582c3464840f1e57a",
67
+ "backbone/README.md": "57e4bdb258ee1a7d2635c5174ebd4e56abe392505cdb5f8bbb356b0dc4293641",
68
+ "backbone/chat_template.jinja": "c3cf9e34abf4f9e36c2d72165aa9c132d3e2a725b6c2586aaa3a8af9d7a81041",
69
+ "backbone/config.json": "191e0af232104ed8b65258cf3fb2b842e288008baca7633c11b82a1ac7203aab",
70
+ "backbone/crc32.txt": "b42dd291f5f98b05807e458f5b47849969a9b3326acbf0c97d28b05826740b83",
71
+ "backbone/generation_config.json": "e70c136c1b78ddc1fb0905bac8e733a4dc448d4f852a5dd75143fffc70be550e",
72
+ "backbone/merges.txt": "a9d356d7bdf1ef4949e3e748e95b8e10ad9d4e2e838eddc38a0a7b6b94d1db8d",
73
+ "backbone/model-00001-of-00018.safetensors": "6c54d794f08d8a479e8a123b6ab3473b8a8f304f0da59b043aabe7bdb5f05a7a",
74
+ "backbone/model-00002-of-00018.safetensors": "3de19e7456388362e7dbe1455362f6b2661d8d7084a7a346e7d3f77053ff0f78",
75
+ "backbone/model-00003-of-00018.safetensors": "103e7f75798c312e696d2547889732b110a634725f6233b7b0bc654bf3365528",
76
+ "backbone/model-00004-of-00018.safetensors": "9b6ff88af2b387c943366b263d1ebef06090cf2a94e637895a74eb10300bf5da",
77
+ "backbone/model-00005-of-00018.safetensors": "f2e95aa5371257fadde49c6d735b716aab00868ae689befd54c091df7a6c57b4",
78
+ "backbone/model-00006-of-00018.safetensors": "7c196564fbb98f683573ae02d3a90bd74ccb7289d9103f09c76d9feffcc64b24",
79
+ "backbone/model-00007-of-00018.safetensors": "1a657de97643d5283547047925453e0bbec151b5f293f0a48ee8fbffb82c83cb",
80
+ "backbone/model-00008-of-00018.safetensors": "e26030a4ee68739f4092696d685b9ef5b4a7052f4843e949cf4419ee7d086b81",
81
+ "backbone/model-00009-of-00018.safetensors": "d73885d7e8995d58cd722b6fd4976035f30e3464189a71d88bb8e504a4612c53",
82
+ "backbone/model-00010-of-00018.safetensors": "ee367c91f6faf3a2d5245e28ee20dbe4e51a1ab08f8e27029c67e7c47a77bd89",
83
+ "backbone/model-00011-of-00018.safetensors": "01ea6406c8ee3f3bd0cbe96fb1ff120bdafc20bdeb4eb5dc8fc08894fef9221f",
84
+ "backbone/model-00012-of-00018.safetensors": "59da9b95e4d818c1189f681044df6bb9a5c7375ea0a01b8d6a6a682451fc4a8e",
85
+ "backbone/model-00013-of-00018.safetensors": "c2f63ec1a168d0f328da8ef74caee72d4e67d6dccfd9ccb595abcaedaeae86c4",
86
+ "backbone/model-00014-of-00018.safetensors": "d8a6453ec4fbe9763644dd9127c3a5ad5fec5c8a5cde6f1414f1101ee9a1250c",
87
+ "backbone/model-00015-of-00018.safetensors": "95db368ab83b8f60d1b9205de6ffa97adbe665d7e61e9adcfb237d25dabf9f8a",
88
+ "backbone/model-00016-of-00018.safetensors": "482d3d948c8cb31068323a85b7c2d445271a28a08699bbed48c13e465894715a",
89
+ "backbone/model-00017-of-00018.safetensors": "7615db84bee928608723f0e5e887f98e11091ccd86583453efcf9745050741f2",
90
+ "backbone/model-00018-of-00018.safetensors": "1219c9ea7951f2bb93c0ccafa2e6e65ac9111fda00961e3fbe614e30050c95e9",
91
+ "backbone/model.safetensors.index.json": "5aec933c5ef08d6c1d411b1b1f225b97d7c55f8eeed4b74535849ce9361e595f",
92
+ "backbone/preprocessor_config.json": "27225450ac9c6529872ee1924fcb0962ff5634834f817040f444118116f4e516",
93
+ "backbone/tokenizer.json": "0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3",
94
+ "backbone/tokenizer_config.json": "b11349aafa7cdc6a320767cf7ceb29ed82f7eda5d65e8e0819e76f0ce947bf27",
95
+ "backbone/video_preprocessor_config.json": "7768af27c1fafa9cc9011c1dc20067e03f8915e03b63504550e11d5066986d13",
96
+ "backbone/vocab.json": "ce99b4cb2983d118806ce0a8b777a35b093e2000a503ebde25853284c9dfa003",
97
+ "heads.npz": "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f"
98
+ },
99
+ "repo_id": "DoccyHealth/Solomon",
100
+ "schema": "solomon-mlx-hub-release-v1",
101
+ "source_hashes": {
102
+ "adapter/adapter.safetensors": "2addaf841ecc58829ad73081603b8d3e52743c53c6d558a17a1dd30e9bd2dbca",
103
+ "adapter/heads.npz": "126a9b5487dca937a768a4f228f2d2e7d513900d8ad0e99c4137fbaa42a1aa6f"
104
+ }
105
+ }
mlx/src/solomon_mlx_hub/prepare.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verified, atomic local assembly without redistributing the Qwen backbone.
2
+
3
+ This separate package leaves the historical converter and running inference code
4
+ unchanged. Conversion uses the original FP32 normalization-offset treatment.
5
+ """
6
+
7
+ import copy
8
+ import fcntl
9
+ import json
10
+ import re
11
+ import shutil
12
+ import tempfile
13
+ from contextlib import contextmanager
14
+ from importlib.metadata import version
15
+ from pathlib import Path, PurePosixPath
16
+
17
+ from huggingface_hub import snapshot_download
18
+
19
+ from solomon_mlx.artifacts import code_identity, sha256
20
+ from solomon_mlx.prepare import convert_bf16, verify_base
21
+
22
+ DATA = Path(__file__).parent / "data"
23
+ RELEASE = json.loads((DATA / "release.json").read_text())
24
+ MANIFEST = DATA / "base-manifest.json"
25
+ SOURCE_PATTERNS = [
26
+ "adapter/**",
27
+ "mlx/bf16/LICENSE",
28
+ "mlx/bf16/NOTICE",
29
+ "mlx/bf16/MODIFICATIONS.md",
30
+ ]
31
+
32
+
33
+ def _revision(revision):
34
+ if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
35
+ raise ValueError("Pin the Solomon repository to a full 40-character commit SHA")
36
+ return revision
37
+
38
+
39
+ def _relative(name):
40
+ path = PurePosixPath(name)
41
+ if not name or path.is_absolute() or ".." in path.parts or "\\" in name:
42
+ raise ValueError("Unsafe artifact path: " + name)
43
+ return path
44
+
45
+
46
+ def _check_converter():
47
+ if code_identity() != RELEASE["converter_code_sha256"]:
48
+ raise ValueError("Converter source identity mismatch; install the pinned source distribution")
49
+ for package, expected in RELEASE["binding_contract"]["dependencies"].items():
50
+ if version(package) != expected:
51
+ raise ValueError(f"Converter requires {package}=={expected}")
52
+ if sha256(MANIFEST) != RELEASE["base_manifest_sha256"]:
53
+ raise ValueError("Pinned base manifest checksum mismatch")
54
+
55
+
56
+ def verify_prepared(directory):
57
+ """Verify an existing local conversion without allocating model tensors.
58
+
59
+ Historical conversions remain usable. Their bindings and byte checksums are
60
+ preserved; a new conversion always receives its own output checksums.
61
+ """
62
+ root = Path(directory).resolve()
63
+ binding = json.loads((root / "binding.json").read_text())
64
+ for key, expected in RELEASE["binding_contract"].items():
65
+ if binding.get(key) != expected:
66
+ raise ValueError("Local binding mismatch: " + key)
67
+ for key, expected in RELEASE["source_hashes"].items():
68
+ if binding.get("source_hashes", {}).get(key) != expected:
69
+ raise ValueError("Local source identity mismatch: " + key)
70
+ files = binding.get("files", {})
71
+ if set(files) != set(RELEASE["reference_files"]):
72
+ raise ValueError("Local conversion has missing or unexpected bound artifacts")
73
+ for name, expected in files.items():
74
+ path = root / _relative(name)
75
+ if not path.resolve().is_relative_to(root) or sha256(path) != expected:
76
+ raise ValueError("Local artifact checksum mismatch: " + name)
77
+ # Safetensors serialization can differ between CPU and Metal. Metadata,
78
+ # processor files, trained adapter and heads must remain byte-identical.
79
+ if (
80
+ not (name.startswith("backbone/") and name.endswith(".safetensors"))
81
+ and expected != RELEASE["reference_files"][name]
82
+ ):
83
+ raise ValueError("Pinned artifact identity mismatch: " + name)
84
+ return binding
85
+
86
+
87
+ @contextmanager
88
+ def _output_lock(output):
89
+ output.parent.mkdir(parents=True, exist_ok=True)
90
+ # Keep the lock inode after release so concurrent waiters cannot bypass it.
91
+ with (output.parent / f".{output.name}.prepare.lock").open("a") as handle:
92
+ try:
93
+ fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
94
+ except BlockingIOError as exc:
95
+ raise RuntimeError("Another process is preparing this output") from exc
96
+ try:
97
+ yield
98
+ finally:
99
+ fcntl.flock(handle, fcntl.LOCK_UN)
100
+
101
+
102
+ def _assemble(snapshot, output, *, revision, base_dir, device):
103
+ source = Path(snapshot)
104
+ for name, expected in RELEASE["source_hashes"].items():
105
+ if sha256(source / _relative(name)) != expected:
106
+ raise ValueError("Solomon source checksum mismatch: " + name)
107
+ for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md"):
108
+ if sha256(source / "mlx/bf16" / name) != RELEASE["reference_files"][name]:
109
+ raise ValueError("Solomon license/provenance checksum mismatch: " + name)
110
+
111
+ manifest = json.loads(MANIFEST.read_text())
112
+ if base_dir is None:
113
+ base_dir = output.parent / ".solomon-sources" / ("base-" + manifest["revision"])
114
+ snapshot_download(
115
+ RELEASE["base_repo_id"],
116
+ revision=manifest["revision"],
117
+ local_dir=base_dir,
118
+ allow_patterns=[row["name"] for row in manifest["files"]],
119
+ max_workers=8,
120
+ )
121
+ base_dir = Path(base_dir)
122
+ base_hashes = verify_base(base_dir, manifest)
123
+ if base_hashes != RELEASE["binding_contract"]["base_hashes"]:
124
+ raise ValueError("Base input identity differs from the original conversion")
125
+
126
+ temporary = Path(tempfile.mkdtemp(prefix=f".{output.name}.", dir=output.parent))
127
+ try:
128
+ import mlx.core as mx
129
+
130
+ # Device selection is scoped to this conversion; CPU conversion also
131
+ # works on Linux. Inference still requires Apple Silicon and Metal.
132
+ with mx.stream(mx.cpu if device == "cpu" else mx.gpu):
133
+ convert_bf16(base_dir, temporary / "backbone")
134
+ for name in RELEASE["source_hashes"]:
135
+ shutil.copy2(source / name, temporary / Path(name).name)
136
+ for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md"):
137
+ shutil.copy2(source / "mlx/bf16" / name, temporary / name)
138
+ binding = copy.deepcopy(RELEASE["binding_contract"])
139
+ binding["source_hashes"] = RELEASE["source_hashes"]
140
+ binding["files"] = {
141
+ str(p.relative_to(temporary)): sha256(p) for p in sorted(temporary.rglob("*")) if p.is_file()
142
+ }
143
+ binding["hub_preparation"] = {
144
+ "repo_id": RELEASE["repo_id"],
145
+ "revision": revision,
146
+ "converter_code_sha256": RELEASE["converter_code_sha256"],
147
+ "loader_code_sha256": sha256(__file__),
148
+ "device": device,
149
+ "base_manifest_sha256": RELEASE["base_manifest_sha256"],
150
+ "calibration": "none; CUDA parity only",
151
+ }
152
+ (temporary / "binding.json").write_text(json.dumps(binding, indent=2, sort_keys=True) + "\n")
153
+ verify_prepared(temporary)
154
+ temporary.rename(output)
155
+ finally:
156
+ if temporary.exists():
157
+ shutil.rmtree(temporary)
158
+ return output
159
+
160
+
161
+ def prepare_from_snapshot(snapshot, output, *, revision, base_dir, device="cpu"):
162
+ """Offline preparation from an adapter-only snapshot and original Qwen files."""
163
+ _revision(revision)
164
+ if base_dir is None:
165
+ raise ValueError("Offline preparation requires the original Qwen base directory")
166
+ if device not in ("cpu", "gpu"):
167
+ raise ValueError("Conversion device must be cpu or gpu")
168
+ _check_converter()
169
+ output = Path(output).absolute()
170
+ with _output_lock(output):
171
+ if output.exists():
172
+ verify_prepared(output)
173
+ return output
174
+ return _assemble(Path(snapshot), output, revision=revision, base_dir=base_dir, device=device)
175
+
176
+
177
+ def prepare_from_hub(output, *, revision, base_dir=None, cache_dir=None, device="cpu"):
178
+ """Download root adapters and pinned base inputs; return a verified local model.
179
+
180
+ Existing valid outputs are reused without network traffic or rewriting their
181
+ runtime identity. Invalid outputs fail closed and are never overwritten.
182
+ """
183
+ _revision(revision)
184
+ if device not in ("cpu", "gpu"):
185
+ raise ValueError("Conversion device must be cpu or gpu")
186
+ _check_converter()
187
+ output = Path(output).absolute()
188
+ with _output_lock(output):
189
+ if output.exists():
190
+ verify_prepared(output)
191
+ return output
192
+ snapshot = snapshot_download(
193
+ RELEASE["repo_id"],
194
+ revision=revision,
195
+ allow_patterns=SOURCE_PATTERNS,
196
+ cache_dir=cache_dir,
197
+ )
198
+ return _assemble(Path(snapshot), output, revision=revision, base_dir=base_dir, device=device)
199
+
200
+
201
+ def load(output, *, revision, base_dir=None, cache_dir=None, device="cpu", **model_options):
202
+ """Prepare the adapter-only release, then load the standard Solomon API."""
203
+ from solomon_mlx import Solomon
204
+
205
+ directory = prepare_from_hub(
206
+ output,
207
+ revision=revision,
208
+ base_dir=base_dir,
209
+ cache_dir=cache_dir,
210
+ device=device,
211
+ )
212
+ return Solomon.load(directory, **model_options)
mlx/tests/test_artifacts_and_evaluation.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from solomon_mlx.artifacts import BASE_REVISION, verify_release
8
+ from solomon_mlx.evaluation import compare_rows, unit
9
+ from solomon_mlx.prepare import verify_base
10
+
11
+
12
+ def test_corrupt_source_and_base_fail_closed(tmp_path):
13
+ path = tmp_path / "input"
14
+ path.write_bytes(b"original")
15
+ sha = hashlib.sha256(b"original").hexdigest()
16
+ (tmp_path / "MANIFEST.json").write_text(
17
+ json.dumps({"files": [{"destination": "input", "staged_bytes": 8, "staged_sha256": sha}]})
18
+ )
19
+ assert verify_release(tmp_path)["input"] == sha
20
+ manifest = {"revision": BASE_REVISION, "files": [{"name": "input", "size": 8, "sha256": sha}]}
21
+ assert verify_base(tmp_path, manifest)["input"] == sha
22
+ path.write_bytes(b"corrupt!")
23
+ with pytest.raises(ValueError):
24
+ verify_release(tmp_path)
25
+ with pytest.raises(ValueError):
26
+ verify_base(tmp_path, manifest)
27
+
28
+
29
+ def test_reserved_choice_exclusion_and_whole_question_accuracy():
30
+ rows = [
31
+ {
32
+ "id": "a",
33
+ "task": "entity",
34
+ "n": 4,
35
+ "head_key": "entity/state4",
36
+ "question_id": "q",
37
+ "gold": 0,
38
+ "letter_logits": [5, 0, 0, 0],
39
+ },
40
+ {
41
+ "id": "b",
42
+ "task": "entity",
43
+ "n": 4,
44
+ "head_key": "entity/state4",
45
+ "question_id": "q",
46
+ "gold": 3,
47
+ "letter_logits": [0, 0, 0, 5],
48
+ },
49
+ ]
50
+ other = [{**rows[0]}, {**rows[1], "letter_logits": [5, 0, 0, 0]}]
51
+ report = compare_rows(rows, other)
52
+ assert report["unit_decision_agreement"] == 0.5
53
+ assert report["mlx_whole_question_accuracy"] == 1
54
+ assert report["cuda_whole_question_accuracy"] == 0
55
+ assert not report["quality_gate_passed"]
56
+ assert unit(
57
+ {"task": "single", "n": 4, "head_key": "single/choiceR", "gold": 3, "letter_logits": [1, 2, 3, 4]}
58
+ ) == (None, None)
59
+
60
+
61
+ def test_norm_offset_preserves_bf16_source_values_in_fp32():
62
+ from types import SimpleNamespace
63
+
64
+ import mlx.core as mx
65
+ from mlx_vlm.models.qwen3_5.qwen3_5 import Model
66
+
67
+ # Sanitization requires config only for tied embeddings and image channels.
68
+ config = SimpleNamespace(
69
+ text_config=SimpleNamespace(tie_word_embeddings=False), vision_config=SimpleNamespace(in_channels=3)
70
+ )
71
+ original = mx.array([0.02, -0.04], dtype=mx.bfloat16)
72
+ converted = Model.sanitize(
73
+ SimpleNamespace(config=config), {"model.language_model.norm.weight": original.astype(mx.float32)}
74
+ )
75
+ out = converted["language_model.model.norm.weight"]
76
+ assert out.dtype == mx.float32
77
+ np.testing.assert_array_equal(np.asarray(out), np.asarray(original.astype(mx.float32)) + 1)
mlx/tests/test_budget.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ import pytest
4
+
5
+ from solomon_mlx.budget import Budget
6
+
7
+
8
+ def test_budget_reserves_before_dispatch_and_exhausts(tmp_path):
9
+ path = tmp_path / "budget.json"
10
+ budget = Budget(path, 20)
11
+ with budget.reserve(15, "one"):
12
+ assert json.loads(path.read_text())["entries"][0]["status"] == "reserved"
13
+ with pytest.raises(RuntimeError), budget.reserve(1, "concurrent"):
14
+ pass
15
+ with pytest.raises(RuntimeError), budget.reserve(6, "too expensive"):
16
+ pass
17
+
18
+
19
+ def test_uncertain_dispatch_blocks_followups(tmp_path):
20
+ budget = Budget(tmp_path / "budget.json")
21
+ with pytest.raises(TimeoutError), budget.reserve(20, "uncertain"):
22
+ raise TimeoutError()
23
+ with pytest.raises(RuntimeError), budget.reserve(20, "retry"):
24
+ pass
mlx/tests/test_contract.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import threading
3
+
4
+ import pytest
5
+
6
+ from solomon_mlx._vendor.contract import parse_questions
7
+ from solomon_mlx._vendor.semantics import p_yes
8
+ from solomon_mlx.api import Solomon, branches, distributions, ordering_score
9
+
10
+
11
+ def test_collapse_before_temperature():
12
+ p = p_yes([0, 0, 0, 0], 2)
13
+ assert p == pytest.approx(1 / (1 + math.sqrt(3)))
14
+ assert p != pytest.approx(0.25)
15
+
16
+
17
+ def test_routing_and_candidate_order():
18
+ specs = parse_questions(
19
+ {
20
+ "single": {"type": "choice", "instructions": "Who?", "options": ["A", "B"]},
21
+ "ordered": {"type": "score", "instructions": "Level?", "levels": ["low", "high"]},
22
+ "entity": {"instructions": "Is {candidate} certified?", "candidates": ["Z", "X"]},
23
+ }
24
+ )
25
+ assert branches(specs[0])[0][1:] == (4, "single/choiceR")
26
+ assert branches(specs[1])[0][1:] == (2, "ordered/choiceS")
27
+ assert "Is Z certified?" in branches(specs[2])[0][0]
28
+ assert distributions(specs[0], [{"letter_logits": [0, 0, 100, 100]}], 1) == [[0.5, 0.5]]
29
+
30
+
31
+ def test_ordering_score_product():
32
+ assert ordering_score([[0.8, 0.2], [0.1, 0.9]]) == pytest.approx(0.72)
33
+ with pytest.raises(ValueError):
34
+ ordering_score([[0.8, 0.3]])
35
+
36
+
37
+ class FakeEngine:
38
+ def __init__(self):
39
+ self.identity = {"fingerprint": "test-only"}
40
+ self.lock = threading.RLock()
41
+ self.prefills = []
42
+
43
+ def prefill(self, parts):
44
+ self.prefills.append(parts)
45
+ return {"parts": parts, "prefix_ids": [1, 2]}
46
+
47
+ def ask(self, state, block, width, head, **kwargs):
48
+ return {
49
+ "letter_logits": [2.0] + [0.0] * (width - 1),
50
+ "branch_tokens": 3,
51
+ "prompt_tokens": 5,
52
+ "head_key": head,
53
+ }
54
+
55
+
56
+ def test_state_ownership_close_and_replay(tmp_path):
57
+ model = Solomon(FakeEngine())
58
+ other = Solomon(FakeEngine())
59
+ state = model.prefill("A fact.")
60
+ recipe = tmp_path / "state.json"
61
+ state.save(recipe)
62
+ with pytest.raises(ValueError):
63
+ other.decide(state=state, questions={"a": "Fact?"})
64
+ state.close()
65
+ with pytest.raises(ValueError):
66
+ model.decide(state=state, questions={"a": "Fact?"})
67
+ with model.replay(recipe) as restored:
68
+ assert restored.prefix_tokens == 2
69
+ recipe.write_text(recipe.read_text().replace("A fact.", "Bad fact."))
70
+ with pytest.raises(ValueError):
71
+ model.replay(recipe)
72
+
73
+
74
+ def test_evidence_budget_stops_fresh_calls():
75
+ engine = FakeEngine()
76
+ model = Solomon(engine)
77
+ with model.prefill("Alice is certified.\nBob is not certified.") as state:
78
+ result = model.decide(
79
+ state=state, questions={"a": "Is Alice certified?"}, evidence="removal", evidence_max_calls=0
80
+ )
81
+ assert result["answers"]["a"]["evidence_status"] == "budget_exhausted"
82
+ assert len(engine.prefills) == 1
83
+
84
+
85
+ def test_evidence_spans_and_fresh_verification():
86
+ engine = FakeEngine()
87
+ model = Solomon(engine)
88
+ text = "Alice is certified.\nBob is not certified."
89
+ with model.prefill(text) as state:
90
+ out = model.decide(state=state, questions={"a": "Is Alice certified?"}, evidence="removal")[
91
+ "answers"
92
+ ]["a"]
93
+ assert len(engine.prefills) == 3
94
+ for span in out["evidence"]:
95
+ assert text[span["start"] : span["end"]] == span["text"]
96
+ assert out["evidence_detail"]["verification"] == "fresh_source_reencoding"
mlx/tests/test_hub.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adapter-only downloads, immutable reuse, integrity failures and atomic assembly."""
2
+
3
+ import copy
4
+ import fcntl
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ import solomon_mlx_hub.prepare as hub
11
+ from solomon_mlx.artifacts import sha256
12
+
13
+ REVISION = "a" * 40
14
+
15
+
16
+ @pytest.fixture
17
+ def release(tmp_path, monkeypatch):
18
+ source, base = tmp_path / "source", tmp_path / "base"
19
+ (source / "adapter").mkdir(parents=True)
20
+ (source / "mlx/bf16").mkdir(parents=True)
21
+ base.mkdir()
22
+ payloads = {"adapter.safetensors": b"trained adapter", "heads.npz": b"trained heads"}
23
+ for name, value in payloads.items():
24
+ (source / "adapter" / name).write_bytes(value)
25
+ for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md"):
26
+ (source / "mlx/bf16" / name).write_text(name)
27
+ (base / "config.json").write_text('{"model_type":"qwen3_5"}')
28
+ manifest = tmp_path / "base-manifest.json"
29
+ manifest.write_text(
30
+ json.dumps(
31
+ {
32
+ "revision": hub.RELEASE["binding_contract"]["base_revision"],
33
+ "files": [
34
+ {
35
+ "name": "config.json",
36
+ "size": (base / "config.json").stat().st_size,
37
+ "sha256": sha256(base / "config.json"),
38
+ }
39
+ ],
40
+ }
41
+ )
42
+ )
43
+ reference = {
44
+ **{name: sha256(source / "adapter" / name) for name in payloads},
45
+ **{name: sha256(source / "mlx/bf16" / name) for name in ("LICENSE", "NOTICE", "MODIFICATIONS.md")},
46
+ "backbone/config.json": sha256(base / "config.json"),
47
+ "backbone/model.safetensors": "historical-serialization-hash",
48
+ }
49
+ spec = copy.deepcopy(hub.RELEASE)
50
+ spec["reference_files"] = reference
51
+ spec["base_manifest_sha256"] = sha256(manifest)
52
+ spec["source_hashes"] = {"adapter/" + name: reference[name] for name in payloads}
53
+ spec["binding_contract"]["base_hashes"] = {"config.json": sha256(base / "config.json")}
54
+ monkeypatch.setattr(hub, "RELEASE", spec)
55
+ monkeypatch.setattr(hub, "MANIFEST", manifest)
56
+ calls = []
57
+
58
+ def convert(root, output):
59
+ assert Path(root) == base
60
+ calls.append("convert")
61
+ output.mkdir()
62
+ (output / "config.json").write_bytes((base / "config.json").read_bytes())
63
+ (output / "model.safetensors").write_bytes(b"new serialization")
64
+
65
+ monkeypatch.setattr(hub, "convert_bf16", convert)
66
+ return source, base, tmp_path / "model", calls
67
+
68
+
69
+ def test_adapter_only_hub_then_verified_offline_reuse(release, monkeypatch):
70
+ source, base, output, calls = release
71
+
72
+ def download(repo_id, **kwargs):
73
+ assert repo_id == "DoccyHealth/Solomon"
74
+ assert kwargs["revision"] == REVISION
75
+ assert "adapter/**" in kwargs["allow_patterns"]
76
+ assert not any(
77
+ "backbone" in pattern or pattern == "mlx/bf16/**" for pattern in kwargs["allow_patterns"]
78
+ )
79
+ calls.append("download")
80
+ return str(source)
81
+
82
+ monkeypatch.setattr(hub, "snapshot_download", download)
83
+ assert hub.prepare_from_hub(output, revision=REVISION, base_dir=base) == output
84
+ binding = hub.verify_prepared(output)
85
+ assert binding["hub_preparation"]["revision"] == REVISION
86
+ assert binding["adapter_scale"] == 2.0
87
+ assert binding["files"]["backbone/model.safetensors"] != "historical-serialization-hash"
88
+ original_binding = (output / "binding.json").read_bytes()
89
+ assert hub.prepare_from_hub(output, revision=REVISION) == output
90
+ assert calls == ["download", "convert"]
91
+ assert (output / "binding.json").read_bytes() == original_binding
92
+ (output / "adapter.safetensors").write_bytes(b"corrupt")
93
+ with pytest.raises(ValueError, match="checksum mismatch"):
94
+ hub.prepare_from_hub(output, revision=REVISION)
95
+ assert calls == ["download", "convert"]
96
+
97
+
98
+ def test_base_download_pinned_and_verified_before_conversion(release, monkeypatch):
99
+ source, _base, output, calls = release
100
+
101
+ def download(repo_id, **kwargs):
102
+ calls.append(repo_id)
103
+ if repo_id == "DoccyHealth/Solomon":
104
+ return source
105
+ assert repo_id == "Qwen/Qwen3.8-27B"
106
+ assert kwargs["revision"] == hub.RELEASE["binding_contract"]["base_revision"]
107
+ target = Path(kwargs["local_dir"])
108
+ target.mkdir(parents=True)
109
+ (target / "config.json").write_bytes(b"corrupt base")
110
+
111
+ monkeypatch.setattr(hub, "snapshot_download", download)
112
+ with pytest.raises(ValueError, match="Base size mismatch"):
113
+ hub.prepare_from_hub(output, revision=REVISION)
114
+ assert calls == ["DoccyHealth/Solomon", "Qwen/Qwen3.8-27B"]
115
+ assert not output.exists()
116
+
117
+
118
+ def test_bad_source_stops_before_base_or_conversion(release):
119
+ source, base, output, calls = release
120
+ (source / "adapter/heads.npz").write_bytes(b"corrupt")
121
+ with pytest.raises(ValueError, match="source checksum mismatch"):
122
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
123
+ assert not calls and not output.exists()
124
+
125
+
126
+ def test_converter_failure_is_atomic_and_retryable(release, monkeypatch):
127
+ source, base, output, calls = release
128
+ convert = hub.convert_bf16
129
+
130
+ def fail(*args):
131
+ args[1].mkdir()
132
+ (args[1] / "partial").write_bytes(b"partial output")
133
+ raise RuntimeError("conversion failed")
134
+
135
+ monkeypatch.setattr(hub, "convert_bf16", fail)
136
+ with pytest.raises(RuntimeError, match="conversion failed"):
137
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
138
+ assert not output.exists()
139
+ assert list(output.parent.glob(".model.*")) == [output.parent / ".model.prepare.lock"]
140
+ monkeypatch.setattr(hub, "convert_bf16", convert)
141
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
142
+ assert calls == ["convert"]
143
+
144
+
145
+ def test_mutable_revision_and_converter_drift_fail_closed(release, monkeypatch):
146
+ source, base, output, calls = release
147
+ for revision in (None, "main", "abc123", "A" * 40):
148
+ with pytest.raises(ValueError, match="commit SHA"):
149
+ hub.prepare_from_hub(output, revision=revision)
150
+ monkeypatch.setattr(hub, "code_identity", lambda: "changed source")
151
+ with pytest.raises(ValueError, match="Converter source identity"):
152
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
153
+ assert not calls
154
+
155
+
156
+ def test_bound_paths_cannot_escape_output(release):
157
+ source, base, output, _calls = release
158
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
159
+ external = output.parent / "external"
160
+ target = output / "backbone/model.safetensors"
161
+ target.rename(external)
162
+ target.symlink_to(external)
163
+ with pytest.raises(ValueError, match="checksum mismatch"):
164
+ hub.verify_prepared(output)
165
+
166
+
167
+ def test_simultaneous_preparations_fail_without_mutation(release):
168
+ source, base, output, calls = release
169
+ with (output.parent / ".model.prepare.lock").open("a") as handle:
170
+ fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
171
+ with pytest.raises(RuntimeError, match="Another process"):
172
+ hub.prepare_from_snapshot(source, output, revision=REVISION, base_dir=base)
173
+ assert not output.exists() and not calls
mlx/tests/test_metal_primitives.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import mlx.core as mx
2
+ import numpy as np
3
+ from mlx import nn
4
+ from mlx_vlm.models.cache import ArraysCache, KVCache
5
+
6
+ from solomon_mlx.engine import SwitchLoRA, fork_cache
7
+
8
+
9
+ def test_question_cache_isolation():
10
+ kv = KVCache()
11
+ kv.update_and_fetch(mx.ones((1, 2, 7, 4)), mx.ones((1, 2, 7, 4)))
12
+ recurrent = ArraysCache(2)
13
+ recurrent.cache = [mx.ones((1, 3, 4)), mx.ones((1, 2, 4, 4), dtype=mx.float32)]
14
+ mx.eval(kv.state, recurrent.state)
15
+ branch = fork_cache([kv, recurrent])
16
+ branch[0].update_and_fetch(mx.full((1, 2, 2, 4), 9), mx.full((1, 2, 2, 4), 8))
17
+ branch[1].cache[0][:] = 12
18
+ branch[1].cache[1][:] = 15
19
+ mx.eval(branch[0].state, branch[1].state)
20
+ assert kv.offset == 7
21
+ np.testing.assert_array_equal(np.asarray(kv.keys[:, :, :7]), 1)
22
+ for array in recurrent.cache:
23
+ np.testing.assert_array_equal(np.asarray(array), 1)
24
+ # A second independent question starts at the original prefix.
25
+ assert fork_cache([kv, recurrent])[0].offset == 7
26
+
27
+
28
+ def test_lora_boundary_multiplier_and_instance_isolation():
29
+ context = {"start": None}
30
+ base = nn.Linear(2, 3, bias=False)
31
+ base.weight = mx.zeros((3, 2), dtype=mx.bfloat16)
32
+ layer = SwitchLoRA(base, mx.ones((2, 1)), mx.ones((1, 3)), context)
33
+ x = mx.ones((1, 4, 2), dtype=mx.bfloat16)
34
+ np.testing.assert_array_equal(np.asarray(layer(x).astype(mx.float32)), 0)
35
+ context["start"] = 2
36
+ out = np.asarray(layer(x).astype(mx.float32))
37
+ np.testing.assert_array_equal(out[:, :2], 0)
38
+ np.testing.assert_array_equal(out[:, 2:], 4)
39
+ other = SwitchLoRA(base, mx.ones((2, 1)), mx.ones((1, 3)), {"start": None})
40
+ np.testing.assert_array_equal(np.asarray(other(x).astype(mx.float32)), 0)
mlx/tests/test_parity.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Keep the user-selected parity workflow free of calibration jobs."""
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+
6
+
7
+ def test_parity_keeps_branches_whose_gold_label_is_reserved():
8
+ script = Path(__file__).resolve().parents[1] / "scripts/check_cuda_parity.py"
9
+ spec = importlib.util.spec_from_file_location("parity_checker", script)
10
+ checker = importlib.util.module_from_spec(spec)
11
+ spec.loader.exec_module(checker)
12
+ row = {
13
+ "task": "single",
14
+ "n": 4,
15
+ "head_key": "single/choiceR",
16
+ "gold": 3,
17
+ "letter_logits": [2, 0, 100, 100],
18
+ }
19
+ probabilities = checker.decision_probabilities(row, 1.0)
20
+ assert len(probabilities) == 2
21
+ assert probabilities.argmax() == 0
22
+ assert probabilities[0] > 0.88
23
+
24
+