hugpy-resources 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hugpy_resources-0.1.3/PKG-INFO +10 -0
- hugpy_resources-0.1.3/README.md +144 -0
- hugpy_resources-0.1.3/pyproject.toml +20 -0
- hugpy_resources-0.1.3/setup.cfg +4 -0
- hugpy_resources-0.1.3/src/hugpy_resources/__init__.py +3 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/__init__.py +35 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/__init__.py +24 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/database.py +49 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/errors.py +74 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/http.py +116 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/nvml.py +32 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/thinking.py +77 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/classes/tool_calls.py +53 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/__init__.py +45 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/admission.py +19 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/architecture.py +84 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/audit.py +6 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/comfy.py +15 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/context.py +37 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/contracts.py +279 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/cpu.py +19 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/drives.py +26 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/engines.py +15 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/formats.py +114 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/gguf.py +88 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/hardware.py +21 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/identity.py +16 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/math.py +38 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/memory.py +31 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/placement.py +140 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/platform.py +29 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/runtime.py +175 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/tensors.py +53 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/terms.py +221 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/text.py +84 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/timing.py +39 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/tool_calls.py +22 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/transfer.py +28 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/constants/validation.py +52 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/__init__.py +157 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/application_paths.py +185 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/argument_parsing.py +27 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/ast_utils.py +26 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/atomic_files.py +25 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/binary_data.py +39 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/block_io.py +78 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/cache.py +92 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/callables.py +10 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/coercion.py +22 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/comfy_metadata.py +101 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/configuration.py +29 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/content_proofs.py +57 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/cpu_activity.py +31 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/devices.py +10 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/engine_metadata.py +43 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/engine_probes.py +217 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/env_files.py +19 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/env_values.py +70 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/environment.py +82 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/evidence.py +37 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/executables.py +51 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/feasibility.py +130 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/file_transfer.py +94 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/file_types.py +256 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/filesystem.py +275 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/filesystems.py +30 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/formatting.py +55 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/geometry.py +217 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/gguf_cache.py +64 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/gguf_headers.py +323 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/gguf_moe.py +356 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/gguf_structure.py +288 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/gguf_variants.py +124 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/hardware_inventory.py +238 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/hashing.py +38 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/host_memory.py +121 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/http_auth.py +36 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/inference_children.py +108 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/inference_timings.py +182 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/integers.py +14 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/json_http.py +22 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/json_rows.py +75 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/json_utils.py +52 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/kv_cache.py +45 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/kv_cells.py +22 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/locking.py +21 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/memory.py +23 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/messages.py +38 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/model_config.py +58 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/model_files.py +94 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/nvml.py +192 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/openai_protocol.py +41 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/paths.py +245 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/placement.py +32 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/ports.py +21 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/processes.py +132 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/profile_metadata.py +30 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/python_environments.py +26 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/quantization.py +50 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/recurrent_state.py +33 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/requirements.py +204 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/row_history.py +77 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/row_values.py +29 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/runtime.py +236 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/safetensors.py +127 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/search.py +884 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/selected_artifacts.py +345 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/source_fingerprints.py +26 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/sql_quoting.py +11 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/sqlite.py +26 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/sqlite_callbacks.py +75 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/sqlite_connections.py +89 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/state.py +41 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/statistics.py +18 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/structured_data.py +72 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/tensor_files.py +193 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/tensors.py +25 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/terminal_text.py +82 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/text.py +104 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/thinking.py +45 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/time_utils.py +37 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/timing.py +415 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/tool_calls.py +86 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/tool_deltas.py +25 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/transformer_facts.py +336 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/units.py +25 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/functions/urls.py +23 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/init_imports.py +80 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/mime_types.json +263 -0
- hugpy_resources-0.1.3/src/hugpy_resources/imports/terms.json +3841 -0
- hugpy_resources-0.1.3/src/hugpy_resources/lazy_imports.py +53 -0
- hugpy_resources-0.1.3/src/hugpy_resources.egg-info/PKG-INFO +10 -0
- hugpy_resources-0.1.3/src/hugpy_resources.egg-info/SOURCES.txt +135 -0
- hugpy_resources-0.1.3/src/hugpy_resources.egg-info/dependency_links.txt +1 -0
- hugpy_resources-0.1.3/src/hugpy_resources.egg-info/requires.txt +7 -0
- hugpy_resources-0.1.3/src/hugpy_resources.egg-info/top_level.txt +1 -0
- hugpy_resources-0.1.3/tests/test_deterministic_tiers.py +135 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hugpy-resources
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Shared generic utilities, file inspection and model resource evidence contracts
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Provides-Extra: inspection
|
|
7
|
+
Requires-Dist: sentencepiece>=0.2; extra == "inspection"
|
|
8
|
+
Requires-Dist: protobuf>=4; extra == "inspection"
|
|
9
|
+
Provides-Extra: yaml
|
|
10
|
+
Requires-Dist: PyYAML; extra == "yaml"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Shared resource evidence
|
|
2
|
+
|
|
3
|
+
`hugpy-resources` owns pure payload formulas, Transformers config geometry and
|
|
4
|
+
resource-term accounting for Discovery and Wrapper. Its base install uses the standard library; it imports no engine, external
|
|
5
|
+
database driver or hardware probe. Shared file inspection and filesystem
|
|
6
|
+
utilities perform their work only when called. Consumer import boundaries resolve the canonical definitions; formula catalog pointers name their actual owners.
|
|
7
|
+
|
|
8
|
+
`imports.functions.requirements.transformer_memory` returns per-device loading and serving terms.
|
|
9
|
+
Their maximum is the required peak; any missing term leaves that peak unknown.
|
|
10
|
+
Historical measurements are retained as component observations. They do not
|
|
11
|
+
prove unrecorded runtime/workload scope or missing peak bounds. Explicit
|
|
12
|
+
`resource_bounds` must be scoped by the caller to the selected artifact,
|
|
13
|
+
runtime, placement and workload. There is currently no persisted producer of
|
|
14
|
+
complete backend bounds, so legacy Transformers facts alone cannot certify a load.
|
|
15
|
+
|
|
16
|
+
`imports.functions.runtime.derive` produces conditional loading and inference tensor reservations
|
|
17
|
+
from shared config geometry and selected safetensors metadata. Wrapper collects
|
|
18
|
+
the largest selected shard and largest BF16/FP16 destination tensor using
|
|
19
|
+
headers/stat only. The same derivation feeds adapter and sizing requirements.
|
|
20
|
+
It does not substitute an observed after-load size for a loading peak. See
|
|
21
|
+
[runtime derivation](RUNTIME_DERIVATION.md) for equations, scope and actual
|
|
22
|
+
inventory results. Backend workspace, loader workspace and allocator overhead
|
|
23
|
+
remain named unknowns until supported evidence supplies them.
|
|
24
|
+
|
|
25
|
+
Wrapper attaches these records to `ModelFootprint`. Unknown requirements refuse
|
|
26
|
+
before resident replacement; the pure commit check compares complete selected
|
|
27
|
+
requirements with captured device/host budgets before unloading victims. The
|
|
28
|
+
existing eviction eligibility and ordering modules retain victim authority.
|
|
29
|
+
|
|
30
|
+
Install this package alongside staged Discovery and Wrapper sources. Wrapper's
|
|
31
|
+
offline Debian builder includes its wheel. Source changes are not deployed by
|
|
32
|
+
editing these files. No tests or runtime load validation were run for this
|
|
33
|
+
refactor; existing database validation records belong to the previous revision.
|
|
34
|
+
|
|
35
|
+
## Source organization
|
|
36
|
+
|
|
37
|
+
The package source is `src/hugpy_resources`. Standard-library and external
|
|
38
|
+
imports go through `imports/init_imports.py`; shared constants live in
|
|
39
|
+
`imports/constants/`. Reusable arithmetic lives in `imports/functions/`,
|
|
40
|
+
grouped into integer validation, tensors, KV cache, recurrent state,
|
|
41
|
+
quantization, placement, memory budgets, configuration, geometry, runtime
|
|
42
|
+
envelopes, evidence, cache ladders, feasibility and requirements.
|
|
43
|
+
|
|
44
|
+
Import implementations directly from `hugpy_resources.imports.functions` or a
|
|
45
|
+
specific module in that package. The former `formulas`, `geometry`, `devices`,
|
|
46
|
+
`runtime`, and `requirements` forwarding modules have been removed. Internal
|
|
47
|
+
imports are relative; shared tables and vocabularies live in `imports/constants/`.
|
|
48
|
+
Optional parser dependencies load only when the corresponding format is read.
|
|
49
|
+
|
|
50
|
+
## Generic function reference
|
|
51
|
+
|
|
52
|
+
Resources is the shared reference for generic utilities used by Discovery.
|
|
53
|
+
Implementations live in `src/hugpy_resources/imports/functions/`; generic
|
|
54
|
+
constants, parsing errors and MIME data live beside them under `imports/`.
|
|
55
|
+
|
|
56
|
+
| Script | Responsibility |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `json_utils.py` | Stable JSON, duplicate-key validation and content digests |
|
|
59
|
+
| `formatting.py` | Human-readable byte counts |
|
|
60
|
+
| `ast_utils.py` | Dotted AST names and JSON-compatible literals |
|
|
61
|
+
| `paths.py` | Path resolution, metadata snapshots and repository-relative paths |
|
|
62
|
+
| `locking.py` | Nonblocking exclusive file locks |
|
|
63
|
+
| `sqlite.py`, `sqlite_connections.py` | SQL identifiers, row writes, read-only connections and WAL retry |
|
|
64
|
+
| `file_types.py` | MIME hints and bounded file-format identification |
|
|
65
|
+
| `tensor_files.py` | Header-only tensor inspection, separate from `tensors.py` arithmetic |
|
|
66
|
+
| `gguf_headers.py`, `gguf_moe.py`, `gguf_structure.py` | GGUF headers, integrity, expert tensors and geometry |
|
|
67
|
+
| `content_proofs.py` | Stable-file SHA256 and Git-blob SHA1, without queues or publication |
|
|
68
|
+
| `source_fingerprints.py` | Deterministic package code/data fingerprints |
|
|
69
|
+
|
|
70
|
+
Use `hugpy_resources.imports` or a specific implementation module. Optional
|
|
71
|
+
SentencePiece/protobuf decoding is installed with `hugpy-resources[inspection]`
|
|
72
|
+
and imported only for that inspection. Generic helpers do not import Discovery,
|
|
73
|
+
Storage, Wrapper or a provider SDK. Consumers supply workflow policy; for
|
|
74
|
+
example, `directory_snapshot(root, excluded_directories=...)` accepts the
|
|
75
|
+
consumer's exclusions.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Tools consume shared implementations
|
|
79
|
+
|
|
80
|
+
`hugpy-tools` imports Resources for its generic file/path operations, structured
|
|
81
|
+
data, hashing, text, time and local search engine. Consumers import Resources directly. The old generic Tools modules are
|
|
82
|
+
removed; tool registration, input schemas, dispatch and backend adapters stay
|
|
83
|
+
in Tools. Resources never imports Tools.
|
|
84
|
+
|
|
85
|
+
Additional generic groups are `filesystem.py`, `hashing.py`,
|
|
86
|
+
`structured_data.py`, `text.py`, `time_utils.py`, `search.py` and `processes.py`.
|
|
87
|
+
Path operations share `paths.py`; safe JSON dumping/key traversal share
|
|
88
|
+
`json_utils.py`; common limits and patterns live in `imports/constants/`.
|
|
89
|
+
`PathEscape` is defined once in `imports/classes.py`.
|
|
90
|
+
|
|
91
|
+
Search's `read_text` keeps its own search-specific result contract; the shared
|
|
92
|
+
`imports.read_text` export names the filesystem reader. Use the search module
|
|
93
|
+
for search operations. Token estimation remains approximate; no model or
|
|
94
|
+
provider package is loaded by the base implementation. YAML parsing is optional
|
|
95
|
+
through `hugpy-resources[yaml]` or Tools' existing YAML extra.
|
|
96
|
+
|
|
97
|
+
`./ops.sh local-pip-update --plan` shows a local install plan. Local installs
|
|
98
|
+
build and archive a new numeric revision. `./ops.sh push-to-pypi --plan` and
|
|
99
|
+
`./ops.sh push-to-github --plan` show the existing release actions; Tools' push
|
|
100
|
+
plan runs Resources first.
|
|
101
|
+
|
|
102
|
+
Generic argument parsing (`coerce_args`, `parse_bool`), SQL quoting
|
|
103
|
+
(`sql_identifier`, `sql_literal`), and explicit environment-file parsing
|
|
104
|
+
(`environment`, `home_dir`, `expand_home`, `parse_env_file`) are grouped under
|
|
105
|
+
`imports/functions/`. `terminal_text.py` supplies shared terminal sanitization,
|
|
106
|
+
Unicode cell widths, clipping, and wrapping; the shared export surface uses
|
|
107
|
+
`clean_terminal_text`, `terminal_cell_width`, `terminal_text_width`,
|
|
108
|
+
`cut_terminal_text`, and `wrap_terminal_text` to distinguish these operations
|
|
109
|
+
from generic text processing.
|
|
110
|
+
|
|
111
|
+
## Suite nomenclature
|
|
112
|
+
|
|
113
|
+
[Resource and storage nomenclature](docs/nomenclature.md) defines store roles,
|
|
114
|
+
T0/T1/T2 capacity tiers, allocation accounting, module ownership, and the
|
|
115
|
+
non-expert-first GPU priority for spill placement. Import `TERM_LABELS`,
|
|
116
|
+
`TERM_DEFINITIONS`, and `PLACEMENT_TERM_ALIASES` from `hugpy_resources.imports`
|
|
117
|
+
for shared strings. MoE names architecture; Spill names the placement policy.
|
|
118
|
+
Legacy placement identifiers remain compatible; the glossary distinguishes
|
|
119
|
+
that policy contract from backend implementation status.
|
|
120
|
+
|
|
121
|
+
`response_shape` is the pure response-structure validator used by Wrapper's
|
|
122
|
+
engine capability probes. It checks chat, embeddings, and explicitly required
|
|
123
|
+
JSON keys without launching engines or interpreting a pass as general model
|
|
124
|
+
compatibility. Probe orchestration and evidence persistence remain in Wrapper.
|
|
125
|
+
|
|
126
|
+
## Consolidated runtime primitives
|
|
127
|
+
|
|
128
|
+
Wrapper, Discovery and Toolserver share implementations below the application
|
|
129
|
+
layer. `imports/classes/` owns exceptions, row/cursor adapters, HTTP scaffolding,
|
|
130
|
+
NVML structures and stream splitters. `imports/constants/` separates evidence
|
|
131
|
+
contracts from shared platform, format and protocol contracts.
|
|
132
|
+
|
|
133
|
+
Environment overrides and application paths live in `env_values`, `environment`
|
|
134
|
+
and `application_paths`. CPU/process/host/NVML readers, model config/identity,
|
|
135
|
+
Comfy declarations, GGUF shard grouping, strict safetensors selection, inference
|
|
136
|
+
timings, reasoning and tool-call parsing are grouped by purpose under
|
|
137
|
+
`imports/functions/`. Similar APIs with different error or identity semantics
|
|
138
|
+
remain separate. `python_environments.provision_python_environment` performs
|
|
139
|
+
explicit installation only when called; the caller owns package/version policy.
|
|
140
|
+
|
|
141
|
+
Resources has no mandatory backend or application dependencies. Standard and
|
|
142
|
+
external imports resolve through `imports/init_imports.py`. Hardware libraries,
|
|
143
|
+
model backends and optional parsers remain deferred to their feature call sites.
|
|
144
|
+
Wrapper-specific placement and quantization preference policies remain in Wrapper.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hugpy-resources"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Shared generic utilities, file inspection and model resource evidence contracts"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[tool.setuptools.packages.find]
|
|
13
|
+
where = ["src"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
inspection = ["sentencepiece>=0.2", "protobuf>=4"]
|
|
17
|
+
yaml = ["PyYAML"]
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.package-data]
|
|
20
|
+
"hugpy_resources.imports" = ["*.json"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Public shared constants, classes and generic utilities.
|
|
2
|
+
|
|
3
|
+
Function/class ownership is declared only in their projections. Constants load
|
|
4
|
+
on first use; optional function implementations load only when requested.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import import_module as _import_module
|
|
8
|
+
|
|
9
|
+
from . import classes as _classes
|
|
10
|
+
from . import functions as _functions
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __getattr__(name):
|
|
14
|
+
if name == "__all__":
|
|
15
|
+
constants = _import_module(f"{__name__}.constants")
|
|
16
|
+
value = list(dict.fromkeys([
|
|
17
|
+
*constants.__all__, *_functions.__all__, *_classes.__all__,
|
|
18
|
+
]))
|
|
19
|
+
elif name in _functions.__all__:
|
|
20
|
+
# A loaded child module can have the same name as an exported function
|
|
21
|
+
# (for example environment). Resolve the declared owner explicitly.
|
|
22
|
+
value = _functions.__getattr__(name)
|
|
23
|
+
elif name in _classes.__all__:
|
|
24
|
+
value = _classes.__getattr__(name)
|
|
25
|
+
else:
|
|
26
|
+
constants = _import_module(f"{__name__}.constants")
|
|
27
|
+
if name not in constants.__all__:
|
|
28
|
+
raise AttributeError(f"{__name__!r} has no attribute {name!r}") from None
|
|
29
|
+
value = getattr(constants, name)
|
|
30
|
+
globals()[name] = value
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __dir__():
|
|
35
|
+
return sorted(set(globals()) | set(__getattr__("__all__")))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Canonical reusable errors, records, protocol parsers and HTTP scaffold."""
|
|
2
|
+
|
|
3
|
+
from hugpy_resources.lazy_imports import import_registry, resolve_import
|
|
4
|
+
|
|
5
|
+
from ..init_imports import import_module
|
|
6
|
+
|
|
7
|
+
_EXPORTS = import_registry({
|
|
8
|
+
'.errors': (
|
|
9
|
+
'GGUFHeaderError PathEscape LocalRequestError ChatRequestError TaskRequestError '
|
|
10
|
+
'StoreError'
|
|
11
|
+
),
|
|
12
|
+
'.database': 'Row Cursor',
|
|
13
|
+
'.nvml': '_MemV2 _Mem _ProcV3',
|
|
14
|
+
'.thinking': 'ThinkSplitter',
|
|
15
|
+
'.tool_calls': 'StreamSplitter',
|
|
16
|
+
'.http': 'make_child_handler',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def __getattr__(name):
|
|
21
|
+
return resolve_import(globals(), _EXPORTS, name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__all__ = list(_EXPORTS)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Reusable database definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Row:
|
|
7
|
+
"""sqlite3.Row compatible access, including duplicate projection names."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, names, values):
|
|
10
|
+
self._names, self._values = tuple(names), tuple(values)
|
|
11
|
+
|
|
12
|
+
def __getitem__(self, key):
|
|
13
|
+
if isinstance(key, (int, slice)):
|
|
14
|
+
return self._values[key]
|
|
15
|
+
try:
|
|
16
|
+
return self._values[self._names.index(key)]
|
|
17
|
+
except ValueError:
|
|
18
|
+
raise KeyError(key) from None
|
|
19
|
+
|
|
20
|
+
def __iter__(self):
|
|
21
|
+
return iter(self._values)
|
|
22
|
+
|
|
23
|
+
def __len__(self):
|
|
24
|
+
return len(self._values)
|
|
25
|
+
|
|
26
|
+
def keys(self):
|
|
27
|
+
return self._names
|
|
28
|
+
|
|
29
|
+
def get(self, key, default=None):
|
|
30
|
+
try:
|
|
31
|
+
return self[key]
|
|
32
|
+
except KeyError:
|
|
33
|
+
return default
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Cursor:
|
|
37
|
+
def __init__(self, rows, rowcount=0, lastrowid=None, columns=(), values=()):
|
|
38
|
+
self.rows = iter(
|
|
39
|
+
[Row(columns, row) for row in values]
|
|
40
|
+
if columns
|
|
41
|
+
else [Row(row.keys(), row.values()) for row in rows]
|
|
42
|
+
)
|
|
43
|
+
self.rowcount, self.lastrowid = rowcount, lastrowid
|
|
44
|
+
|
|
45
|
+
def fetchone(self):
|
|
46
|
+
return next(self.rows, None)
|
|
47
|
+
|
|
48
|
+
def fetchall(self):
|
|
49
|
+
return list(self.rows)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Reusable parsing errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GGUFHeaderError(ValueError):
|
|
5
|
+
"""The GGUF header is unreadable (magic, version, extent or EOF)."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ["GGUFHeaderError"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PathEscape(ValueError):
|
|
12
|
+
"""A path resolved outside the confinement root."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ += ["PathEscape"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LocalRequestError(Exception):
|
|
19
|
+
"""Fallback request error so a runner / child module is importable and
|
|
20
|
+
py_compile-clean on its own (e.g. a unit test imports ``handle`` directly).
|
|
21
|
+
|
|
22
|
+
The child process INJECTS its own ``RequestError`` class into the runner's
|
|
23
|
+
namespace before serving; that class carries the same ``(status, message)``
|
|
24
|
+
and a ``.body`` dict, so ``raise RequestError(400, …)`` stays catchable.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, status: int, message: str, **extra):
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
self.status = int(status)
|
|
30
|
+
self.message = message
|
|
31
|
+
self.extra = extra
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def body(self) -> dict:
|
|
35
|
+
err = {
|
|
36
|
+
"message": self.message,
|
|
37
|
+
"type": "invalid_request_error",
|
|
38
|
+
"code": self.status,
|
|
39
|
+
}
|
|
40
|
+
err.update(self.extra)
|
|
41
|
+
return {"error": err}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ChatRequestError(Exception):
|
|
45
|
+
"""An OpenAI-style client error: HTTP ``status`` + error body."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self, status: int, message: str, etype: str = "invalid_request_error", **extra
|
|
49
|
+
):
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
self.status = status
|
|
52
|
+
self.body = {
|
|
53
|
+
"error": {"code": status, "type": etype, "message": message, **extra}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TaskRequestError(Exception):
|
|
58
|
+
"""An OpenAI-style client error: HTTP ``status`` + error body. INJECTED into
|
|
59
|
+
the runner module's namespace before use, so a runner raises
|
|
60
|
+
``RequestError(400, …)`` and it is caught here as a 4xx (never a 500)."""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self, status: int, message: str, etype: str = "invalid_request_error", **extra
|
|
64
|
+
):
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self.status = int(status)
|
|
67
|
+
self.message = message
|
|
68
|
+
self.body = {
|
|
69
|
+
"error": {"code": int(status), "type": etype, "message": message, **extra}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class StoreError(RuntimeError):
|
|
74
|
+
"""The store could not be opened or migrated."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""HTTP scaffold for engine children."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from ..init_imports import json
|
|
5
|
+
from ..init_imports import traceback
|
|
6
|
+
from ..init_imports import BaseHTTPRequestHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _not_found(path: str) -> dict:
|
|
10
|
+
return {
|
|
11
|
+
"error": {"code": 404, "type": "not_found_error", "message": f"no route {path}"}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def make_child_handler(server, request_error_cls, *, chat: bool = True, log=None):
|
|
16
|
+
"""Build the ``BaseHTTPRequestHandler`` subclass the child model servers use.
|
|
17
|
+
|
|
18
|
+
Shared across tf_child / vision_child / task_child: ``log_message`` (silent —
|
|
19
|
+
stderr is the loader-log channel), ``_json``, and the GET routes
|
|
20
|
+
(``/health``, ``/v1/models``, ``/props``). With ``chat=True`` (tf/vision) the
|
|
21
|
+
POST route is ``/v1/chat/completions``, errors fall back to a 500 (logged via
|
|
22
|
+
``log`` if given), and the subclass supplies ``_stream``; with ``chat=False``
|
|
23
|
+
(task) the subclass supplies its own ``do_POST``.
|
|
24
|
+
|
|
25
|
+
``server`` must expose ``models_doc()``/``props_doc()`` and, for chat,
|
|
26
|
+
``lock``/``prepare(body)``/``complete(job)``. ``request_error_cls`` is the
|
|
27
|
+
child's injected ``RequestError``; ``log`` is the child's ``_log``.
|
|
28
|
+
"""
|
|
29
|
+
RequestError = request_error_cls
|
|
30
|
+
|
|
31
|
+
class _Handler(BaseHTTPRequestHandler):
|
|
32
|
+
protocol_version = "HTTP/1.0"
|
|
33
|
+
|
|
34
|
+
def log_message(self, fmt, *args): # keep the loader log for errors only
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def _json(self, status: int, doc: dict) -> None:
|
|
38
|
+
data = json.dumps(doc).encode()
|
|
39
|
+
self.send_response(status)
|
|
40
|
+
self.send_header("Content-Type", "application/json")
|
|
41
|
+
self.send_header("Content-Length", str(len(data)))
|
|
42
|
+
self.end_headers()
|
|
43
|
+
self.wfile.write(data)
|
|
44
|
+
|
|
45
|
+
def do_GET(self):
|
|
46
|
+
path = self.path.split("?", 1)[0]
|
|
47
|
+
if path == "/health":
|
|
48
|
+
return self._json(200, {"status": "ok"})
|
|
49
|
+
if path == "/v1/models":
|
|
50
|
+
return self._json(200, server.models_doc())
|
|
51
|
+
if path == "/props":
|
|
52
|
+
return self._json(200, server.props_doc())
|
|
53
|
+
return self._json(404, _not_found(path))
|
|
54
|
+
|
|
55
|
+
if chat:
|
|
56
|
+
|
|
57
|
+
def do_POST(self):
|
|
58
|
+
path = self.path.split("?", 1)[0]
|
|
59
|
+
if path not in ("/v1/chat/completions", "/tokenize"):
|
|
60
|
+
return self._json(404, _not_found(path))
|
|
61
|
+
try:
|
|
62
|
+
n = int(self.headers.get("Content-Length") or 0)
|
|
63
|
+
body = json.loads(self.rfile.read(n) or b"{}")
|
|
64
|
+
if not isinstance(body, dict):
|
|
65
|
+
raise RequestError(400, "request body must be a JSON object")
|
|
66
|
+
except (ValueError, RequestError) as exc:
|
|
67
|
+
err = (
|
|
68
|
+
exc
|
|
69
|
+
if isinstance(exc, RequestError)
|
|
70
|
+
else RequestError(400, f"bad JSON: {exc}")
|
|
71
|
+
)
|
|
72
|
+
return self._json(err.status, err.body)
|
|
73
|
+
if path == "/tokenize":
|
|
74
|
+
tokenize = getattr(server, "tokenize_doc", None)
|
|
75
|
+
if tokenize is None:
|
|
76
|
+
return self._json(404, _not_found(path))
|
|
77
|
+
if not server.lock.acquire(blocking=False):
|
|
78
|
+
return self._json(
|
|
79
|
+
409,
|
|
80
|
+
{
|
|
81
|
+
"error": {
|
|
82
|
+
"message": "Tokenizer preflight deferred while the engine is busy"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
return self._json(200, tokenize(body))
|
|
88
|
+
except RequestError as exc:
|
|
89
|
+
return self._json(exc.status, exc.body)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
return self._json(400, {"error": {"message": str(exc)}})
|
|
92
|
+
finally:
|
|
93
|
+
server.lock.release()
|
|
94
|
+
with server.lock:
|
|
95
|
+
try:
|
|
96
|
+
job = server.prepare(body)
|
|
97
|
+
if not body.get("stream"):
|
|
98
|
+
return self._json(200, server.complete(job))
|
|
99
|
+
return self._stream(job)
|
|
100
|
+
except RequestError as exc:
|
|
101
|
+
return self._json(exc.status, exc.body)
|
|
102
|
+
except Exception as exc: # noqa: BLE001 — the real error, to the caller and the log
|
|
103
|
+
if log:
|
|
104
|
+
log(traceback.format_exc())
|
|
105
|
+
return self._json(
|
|
106
|
+
500,
|
|
107
|
+
{
|
|
108
|
+
"error": {
|
|
109
|
+
"code": 500,
|
|
110
|
+
"type": "server_error",
|
|
111
|
+
"message": f"{type(exc).__name__}: {exc}",
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return _Handler
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Reusable nvml definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..init_imports import ctypes
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class _MemV2(ctypes.Structure):
|
|
9
|
+
_fields_ = [
|
|
10
|
+
("version", ctypes.c_uint),
|
|
11
|
+
("total", ctypes.c_ulonglong),
|
|
12
|
+
("reserved", ctypes.c_ulonglong),
|
|
13
|
+
("free", ctypes.c_ulonglong),
|
|
14
|
+
("used", ctypes.c_ulonglong),
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _Mem(ctypes.Structure):
|
|
19
|
+
_fields_ = [
|
|
20
|
+
("total", ctypes.c_ulonglong),
|
|
21
|
+
("free", ctypes.c_ulonglong),
|
|
22
|
+
("used", ctypes.c_ulonglong),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _ProcV3(ctypes.Structure):
|
|
27
|
+
_fields_ = [
|
|
28
|
+
("pid", ctypes.c_uint),
|
|
29
|
+
("usedGpuMemory", ctypes.c_ulonglong),
|
|
30
|
+
("gpuInstanceId", ctypes.c_uint),
|
|
31
|
+
("computeInstanceId", ctypes.c_uint),
|
|
32
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Streaming sibling of no_think.strip_think: separate a model's <think> reasoning
|
|
2
|
+
# from its answer prose INCREMENTALLY, as content deltas arrive, so the front
|
|
3
|
+
# door can re-emit delta.reasoning_content vs delta.content live. The engine
|
|
4
|
+
# (llama-server --reasoning-format deepseek) usually separates reasoning itself;
|
|
5
|
+
# this is the fallback for inline-tag replies (reasoning-format none / a model
|
|
6
|
+
# whose template leaves <think> tags in message.content).
|
|
7
|
+
#
|
|
8
|
+
# Final-assembly semantics match no_think.strip_think (<think>...</think> blocks
|
|
9
|
+
# and an unclosed <think>...EOF are reasoning). An ORPHAN </think> (a template
|
|
10
|
+
# that opens <think> in the prompt, so the reply starts mid-reasoning with no
|
|
11
|
+
# opening tag) is handled downstream by hugpy_core.grader.strip_think on the
|
|
12
|
+
# assembled text; live, text before an orphan close streams as content and the
|
|
13
|
+
# answer after it streams as content too — the grader's rule salvages the answer.
|
|
14
|
+
"""Incremental <think> splitter for streamed content."""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from ..constants import THINK_CLOSE_TAG as _CLOSE
|
|
19
|
+
from ..constants import THINK_OPEN_TAG as _OPEN
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from ..functions.thinking import _partial_tail
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ThinkSplitter:
|
|
26
|
+
"""Feed streamed content text; get back (channel, chunk) pairs where channel
|
|
27
|
+
is 'reasoning' (inside a <think> block) or 'content' (the answer). Holds back
|
|
28
|
+
a short tail when the buffer might end mid-tag, so a tag split across two
|
|
29
|
+
feeds is still recognised."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
self._buf = ""
|
|
33
|
+
self._mode = "content" # 'content' | 'reasoning'
|
|
34
|
+
|
|
35
|
+
def feed(self, text: str) -> "list[tuple[str, str]]":
|
|
36
|
+
out: "list[tuple[str, str]]" = []
|
|
37
|
+
if not text:
|
|
38
|
+
return out
|
|
39
|
+
self._buf += text
|
|
40
|
+
while True:
|
|
41
|
+
low = self._buf.lower()
|
|
42
|
+
i_open = low.find(_OPEN)
|
|
43
|
+
i_close = low.find(_CLOSE)
|
|
44
|
+
cands = [p for p in (i_open, i_close) if p != -1]
|
|
45
|
+
if cands:
|
|
46
|
+
idx = min(cands)
|
|
47
|
+
is_open = idx == i_open
|
|
48
|
+
tag = _OPEN if is_open else _CLOSE
|
|
49
|
+
before = self._buf[:idx]
|
|
50
|
+
if before:
|
|
51
|
+
out.append((self._mode, before))
|
|
52
|
+
self._buf = self._buf[idx + len(tag) :]
|
|
53
|
+
self._mode = "reasoning" if is_open else "content"
|
|
54
|
+
continue
|
|
55
|
+
hold = _partial_tail(self._buf)
|
|
56
|
+
if hold:
|
|
57
|
+
emit = self._buf[: len(self._buf) - hold]
|
|
58
|
+
if emit:
|
|
59
|
+
out.append((self._mode, emit))
|
|
60
|
+
self._buf = self._buf[len(self._buf) - hold :]
|
|
61
|
+
else:
|
|
62
|
+
if self._buf:
|
|
63
|
+
out.append((self._mode, self._buf))
|
|
64
|
+
self._buf = ""
|
|
65
|
+
break
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
def close(self) -> "list[tuple[str, str]]":
|
|
69
|
+
"""Flush whatever is held (a dangling partial tag, treated as text)."""
|
|
70
|
+
out: "list[tuple[str, str]]" = []
|
|
71
|
+
if self._buf:
|
|
72
|
+
out.append((self._mode, self._buf))
|
|
73
|
+
self._buf = ""
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__all__ = ["ThinkSplitter"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Reusable tool calls definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..functions.tool_calls import _calls
|
|
6
|
+
from ..constants import _BLOCK
|
|
7
|
+
from ..constants import DSML_TOOLS_TAGS as _TAGS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StreamSplitter:
|
|
11
|
+
"""Streamed reply: feed() content pieces; yields ("content", text) for prose and
|
|
12
|
+
("tool_calls", [call, ...]) once a whole block has arrived. Text that may be the
|
|
13
|
+
start of a block is held back until it is known not to be one."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.buf = ""
|
|
17
|
+
self.emitted = 0 # tool calls sent so far (their stream index)
|
|
18
|
+
|
|
19
|
+
def _hold_from(self) -> int:
|
|
20
|
+
"""Index from which the buffer must be held (a block, or a possible tag start)."""
|
|
21
|
+
starts = [i for tag in _TAGS if (i := self.buf.find(tag)) >= 0]
|
|
22
|
+
if starts:
|
|
23
|
+
return min(starts)
|
|
24
|
+
for k in range(min(max(map(len, _TAGS)) - 1, len(self.buf)), 0, -1):
|
|
25
|
+
if any(tag.startswith(self.buf[-k:]) for tag in _TAGS):
|
|
26
|
+
return len(self.buf) - k
|
|
27
|
+
return len(self.buf)
|
|
28
|
+
|
|
29
|
+
def feed(self, piece: str):
|
|
30
|
+
self.buf += piece or ""
|
|
31
|
+
out = []
|
|
32
|
+
while True:
|
|
33
|
+
m = _BLOCK.search(self.buf)
|
|
34
|
+
if m:
|
|
35
|
+
if m.start():
|
|
36
|
+
out.append(("content", self.buf[: m.start()]))
|
|
37
|
+
calls = _calls(m.group(2))
|
|
38
|
+
if calls:
|
|
39
|
+
out.append(("tool_calls", calls))
|
|
40
|
+
else:
|
|
41
|
+
out.append(("content", m.group(0)))
|
|
42
|
+
self.buf = self.buf[m.end() :]
|
|
43
|
+
continue
|
|
44
|
+
cut = self._hold_from()
|
|
45
|
+
if cut:
|
|
46
|
+
out.append(("content", self.buf[:cut]))
|
|
47
|
+
self.buf = self.buf[cut:]
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
def close(self):
|
|
51
|
+
"""End of stream: an unfinished block is passed through as text (never lost)."""
|
|
52
|
+
rest, self.buf = self.buf, ""
|
|
53
|
+
return [("content", rest)] if rest else []
|