anolisa-tokenless 0.7.10 → 0.7.12
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.
- package/README.md +62 -18
- package/adapters/tokenless/claude-code/.claude-plugin/plugin.json +1 -1
- package/adapters/tokenless/codex/.codex-plugin/plugin.json +1 -1
- package/adapters/tokenless/common/cosh-extension.json +1 -1
- package/adapters/tokenless/common/hooks/compress_schema_hook.py +194 -13
- package/adapters/tokenless/dsh/package.json +1 -1
- package/adapters/tokenless/hermes/plugin.yaml +1 -1
- package/adapters/tokenless/manifest.json +1 -1
- package/adapters/tokenless/openclaw/openclaw.plugin.json +1 -1
- package/adapters/tokenless/openclaw/package.json +1 -1
- package/adapters/tokenless/qoder/.qoder-plugin/plugin.json +1 -1
- package/adapters/tokenless/qwencode/qwen-extension.json +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
Token-Less combines complementary strategies to minimize LLM token consumption:
|
|
8
8
|
|
|
9
9
|
- **Schema & Response Compression** — Compresses OpenAI Function Calling tool definitions and API responses via the `tokenless-schema` library, cutting structural overhead before tokens ever reach the context window.
|
|
10
|
-
- **TOON Context Compression** — Encodes JSON responses to TOON (Token-Oriented Object Notation) format via the `toon-format` library linked into `tokenless`, reducing
|
|
10
|
+
- **TOON Context Compression** — Encodes JSON responses to TOON (Token-Oriented Object Notation) format via the `toon-format` library linked into `tokenless`, reducing syntax overhead for suitable structured data.
|
|
11
11
|
- **Command Rewriting** — Integrates [RTK](https://github.com/rtk-ai/rtk) to filter and rewrite CLI command output, eliminating noise that would otherwise waste 60–90% of tokens.
|
|
12
12
|
- **Tool Ready (legacy, hard-disabled)** — Its pre-call dependency checks are retained in source but unconditionally bypassed while the readiness model is redesigned.
|
|
13
13
|
|
|
@@ -27,12 +27,12 @@ cover schema compression, RTK rewriting, response compression, TOON, retrieval,
|
|
|
27
27
|
|
|
28
28
|
## Features
|
|
29
29
|
|
|
30
|
-
| Capability |
|
|
30
|
+
| Capability | Savings indicator | Details |
|
|
31
31
|
|---|---|---|
|
|
32
|
-
| Schema compression |
|
|
33
|
-
| Response compression |
|
|
32
|
+
| Schema compression | 47.3% on reference fixture | Compresses OpenAI Function Calling tool schemas |
|
|
33
|
+
| Response compression | 65.8% on reference fixture | Compresses API / tool responses |
|
|
34
34
|
| Reversible compression (stash) | — | Dropped array items are stashed and retrievable via `<<tokenless:KEY>>` markers |
|
|
35
|
-
| TOON context compression |
|
|
35
|
+
| TOON context compression | 17.0% on reference response | Encodes JSON to TOON format for LLMs |
|
|
36
36
|
| Command rewriting | 60–90% | Filters CLI output via RTK (70+ commands supported) |
|
|
37
37
|
| Tool Ready | reduces retry waste | Legacy pre-call check, auto-fix, and blocking; hard-disabled |
|
|
38
38
|
| OpenClaw plugin | — | Command rewriting ✅, Response compression ✅, Schema compression ✅ |
|
|
@@ -46,23 +46,40 @@ cover schema compression, RTK rewriting, response compression, TOON, retrieval,
|
|
|
46
46
|
| AgentScope framework integration | — | Schema ✅, RTK ✅, Response ✅, TOON ✅, Retrieval ✅ |
|
|
47
47
|
| Zero runtime deps | — | Pure Rust, single static binary |
|
|
48
48
|
|
|
49
|
+
The schema, response, and TOON figures above are isolated Tokenless 0.7.11
|
|
50
|
+
results on the repository's committed reference fixtures; they are neither a
|
|
51
|
+
production range nor additive. Compression depends on payload size and shape,
|
|
52
|
+
removable fields, configured thresholds, and the share of tool data in the
|
|
53
|
+
session. Short or already compact payloads may save only a few percent or pass
|
|
54
|
+
through unchanged. See [Measuring Tokenless Savings](../../docs/user-guide/en/token-saving/tokenless/measuring-savings.md#run-the-repository-reference-workload)
|
|
55
|
+
for the exact inputs, command, full output, and limitations.
|
|
56
|
+
|
|
49
57
|
## Applicable Scenarios & Expected Effects
|
|
50
58
|
|
|
51
|
-
tokenless
|
|
59
|
+
tokenless optimizes the tool-related content it handles—tool schemas, tool/API
|
|
60
|
+
responses, and supported shell output—before it enters the LLM context. It does
|
|
61
|
+
not touch model reasoning or conversation history. The payoff depends heavily
|
|
62
|
+
on the share and shape of that content in the session.
|
|
52
63
|
|
|
53
64
|
### Where it pays off
|
|
54
65
|
|
|
55
66
|
| Workload | Primary strategy | Why |
|
|
56
67
|
|----------|-----------------|-----|
|
|
57
68
|
| Shell-heavy (build/test/triage) | Command rewriting (RTK) | `cargo`/`npm`/`go`/`pytest` output carries lots of progress/warning noise; RTK cuts 60–90% |
|
|
58
|
-
| API/fetch-heavy (REST, web_fetch) | Response compression + TOON | JSON
|
|
59
|
-
| Agents with many tools | Schema compression | Many Function Calling definitions carry verbose descriptions
|
|
69
|
+
| API/fetch-heavy (REST, web_fetch) | Response compression + TOON | JSON may carry removable debug/null/empty fields; sufficiently large, regular structures also have reducible syntax overhead |
|
|
70
|
+
| Agents with many tools | Schema compression | Many Function Calling definitions carry verbose descriptions and removable metadata |
|
|
60
71
|
| Long responses that must stay faithful | Reversible compression (Stash) | Truncated content is `retrieve`-able end-to-end lossless; thresholds can be tightened safely |
|
|
61
72
|
|
|
62
73
|
### Where it pays little or doesn't apply
|
|
63
74
|
|
|
64
75
|
- **Chat-heavy / few tool calls**: tool-response share is tiny, overall savings approach 0.
|
|
65
|
-
- **
|
|
76
|
+
- **No fixed minimum payload**: `compress-schema` and `compress-response` build a
|
|
77
|
+
candidate for every accepted valid JSON input. In active mode, they emit it only
|
|
78
|
+
when its estimated token count is strictly lower than the original. A small input
|
|
79
|
+
with removable content can still compress, while a larger already-compact input can
|
|
80
|
+
pass through unchanged; the CLI writes the reason to stderr and records no stats.
|
|
81
|
+
In dry-run mode, the CLI always emits the original and may record a smaller candidate
|
|
82
|
+
as a predicted saving.
|
|
66
83
|
- **Model inference tokens / billed tokens**: outside what tokenless touches.
|
|
67
84
|
|
|
68
85
|
### Estimating the effect
|
|
@@ -232,6 +249,14 @@ Tokenless session second.
|
|
|
232
249
|
|
|
233
250
|
## CLI Usage
|
|
234
251
|
|
|
252
|
+
The standalone `compress-schema` and `compress-response` commands use this
|
|
253
|
+
content-dependent savings check rather than a fixed byte or character minimum.
|
|
254
|
+
The description, string, array, and depth limits in the
|
|
255
|
+
[CLI reference](../../docs/user-guide/en/token-saving/tokenless/cli-reference.md)
|
|
256
|
+
trigger individual transformations; they are not minimum total payload sizes.
|
|
257
|
+
Agent adapters may apply separate pre-check thresholds; see the
|
|
258
|
+
[framework integration guide](../../docs/user-guide/en/token-saving/tokenless/framework-integration.md#adapter-processing-rules).
|
|
259
|
+
|
|
235
260
|
### compress-schema
|
|
236
261
|
|
|
237
262
|
Compress a single tool schema:
|
|
@@ -250,6 +275,15 @@ Compress a batch of tools (JSON array):
|
|
|
250
275
|
tokenless compress-schema -f tools.json --batch
|
|
251
276
|
```
|
|
252
277
|
|
|
278
|
+
A top-level request object with a `tools` array is also accepted without
|
|
279
|
+
`--batch`; OpenAI wrappers, Gemini `functionDeclarations` tool objects, and
|
|
280
|
+
bare Function Calling declarations are compressed while non-function tools and
|
|
281
|
+
fields outside `tools` are preserved:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
tokenless compress-schema -f request.json
|
|
285
|
+
```
|
|
286
|
+
|
|
253
287
|
### compress-response
|
|
254
288
|
|
|
255
289
|
Compress an API response:
|
|
@@ -262,8 +296,12 @@ tokenless compress-response -f response.json
|
|
|
262
296
|
curl -s https://api.example.com/data | tokenless compress-response
|
|
263
297
|
```
|
|
264
298
|
|
|
265
|
-
|
|
266
|
-
|
|
299
|
+
Long arrays are truncated to a head+tail window: the first
|
|
300
|
+
`--truncate-arrays-at` items (default 32) plus the last
|
|
301
|
+
`--array-tail-preserve` items (default 8), with a truncation marker in
|
|
302
|
+
between; pass `--array-tail-preserve 0` for head-only truncation. By default
|
|
303
|
+
`compress-response` stashes the dropped middle segment so it can be retrieved
|
|
304
|
+
later (see [Reversible compression](docs/stash-reversible-compression.md)).
|
|
267
305
|
Pass `--no-stash` for lossy truncation, or `--stash-db <path>` to override the
|
|
268
306
|
stash database (default `~/.tokenless/stash.db`).
|
|
269
307
|
|
|
@@ -278,7 +316,7 @@ tokenless retrieve c30ccf5ed1125e0ed871ba8e
|
|
|
278
316
|
|
|
279
317
|
# Or paste the whole truncation line — the hash is extracted automatically.
|
|
280
318
|
# (Use the FULL 24-hex hash from your output; the value below is shorthand.)
|
|
281
|
-
tokenless retrieve "<...
|
|
319
|
+
tokenless retrieve "<... 160 items truncated, retrieve with <<tokenless:c30ccf5ed1125e0ed871ba8e>>"
|
|
282
320
|
```
|
|
283
321
|
|
|
284
322
|
### compress-toon / decompress-toon
|
|
@@ -298,10 +336,13 @@ echo 'name: Alice\nage: 30' | tokenless decompress-toon
|
|
|
298
336
|
|
|
299
337
|
### Inspect token savings
|
|
300
338
|
|
|
301
|
-
Use `show`
|
|
302
|
-
explain the estimated token saving and highlight only changed lines:
|
|
339
|
+
Use `stats summary` for totals, `show` for the stored before/after payload, or
|
|
340
|
+
`diff` to explain the estimated token saving and highlight only changed lines:
|
|
303
341
|
|
|
304
342
|
```bash
|
|
343
|
+
tokenless stats summary
|
|
344
|
+
tokenless stats summary --limit 1000
|
|
345
|
+
tokenless stats summary --compare <baseline-session> <active-session>
|
|
305
346
|
tokenless stats show 42
|
|
306
347
|
tokenless stats diff 42
|
|
307
348
|
tokenless stats diff --session <session-id>
|
|
@@ -309,10 +350,13 @@ tokenless stats diff --session <session-id> --tool-use-id <tool-use-id>
|
|
|
309
350
|
tokenless stats diff 42 --json
|
|
310
351
|
```
|
|
311
352
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
353
|
+
`stats summary --limit` must be a positive integer; `--limit 0` is rejected at
|
|
354
|
+
parse time. `--compare` fails if either session has no records instead of
|
|
355
|
+
reporting 0% savings. Session overviews contain metrics only. Record and
|
|
356
|
+
tool-use reports include a unified content diff; consecutive active stages are
|
|
357
|
+
linked only when their stored output/input content matches exactly, avoiding
|
|
358
|
+
duplicate intermediate token counts. See
|
|
359
|
+
[Measuring Tokenless Savings](../../docs/user-guide/en/token-saving/tokenless/measuring-savings.md)
|
|
316
360
|
for options and measurement limits.
|
|
317
361
|
|
|
318
362
|
### Database location
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenless",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Token-Less context compression for Claude Code — RTK command rewriting, response/TOON compression, and Tool Ready environment pre-check",
|
|
5
5
|
"author": { "name": "ANOLISA" },
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenless",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Intelligent tool response compression, environment error detection, and token optimization. Strips noise (debug fields, nulls, empty values), truncates long strings/arrays, applies TOON encoding for JSON responses, and classifies environment errors with actionable fix hints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"compression",
|
|
@@ -11,11 +11,19 @@ The agent ID is read from the TOKENLESS_AGENT_ID environment variable
|
|
|
11
11
|
(set by the install action script).
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
14
17
|
import json
|
|
15
18
|
import os
|
|
16
19
|
import subprocess
|
|
17
20
|
import sys
|
|
18
21
|
|
|
22
|
+
try: # POSIX hosts (cosh / Cosh-NG) — the platforms these hooks target.
|
|
23
|
+
import fcntl
|
|
24
|
+
except ImportError: # pragma: no cover - non-POSIX fallback keeps best-effort
|
|
25
|
+
fcntl = None
|
|
26
|
+
|
|
19
27
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
20
28
|
|
|
21
29
|
from hook_utils import (
|
|
@@ -25,6 +33,7 @@ from hook_utils import (
|
|
|
25
33
|
resolve_agent_id,
|
|
26
34
|
resolve_binary,
|
|
27
35
|
resolve_tool_call_id,
|
|
36
|
+
secure_write_text,
|
|
28
37
|
skip,
|
|
29
38
|
warn,
|
|
30
39
|
)
|
|
@@ -33,6 +42,63 @@ from hook_utils import (
|
|
|
33
42
|
|
|
34
43
|
_AGENT_ID = resolve_agent_id()
|
|
35
44
|
|
|
45
|
+
# One marker file holds the session keys that already emitted the "no tool
|
|
46
|
+
# declarations" warning — one key per line, most recent last — so the warning
|
|
47
|
+
# repeats at most once per session even though BeforeModel fires on every
|
|
48
|
+
# model turn. A single-value marker would not survive concurrent sessions
|
|
49
|
+
# under one HOME: session B's first warning would overwrite session A's key
|
|
50
|
+
# and make A warn again on its next turn. The list is bounded (oldest entries
|
|
51
|
+
# trimmed) so it cannot grow without limit; losing an evicted key only
|
|
52
|
+
# re-arms the warning for a long-idle session. All reads and writes of the
|
|
53
|
+
# marker run under the exclusive sidecar lock below: an unlocked
|
|
54
|
+
# read-modify-write lets two hook processes read the same old key set and
|
|
55
|
+
# write back over each other, silently losing one session's key.
|
|
56
|
+
_NO_TOOLS_WARN_MARKER = os.path.join(
|
|
57
|
+
os.path.expanduser("~"), ".tokenless", ".schema-hook-nowarn-session"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Best-effort dedup bound: far above any plausible number of concurrently
|
|
61
|
+
# active sessions sharing one HOME.
|
|
62
|
+
_MAX_WARNED_SESSIONS = 64
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@contextlib.contextmanager
|
|
66
|
+
def _marker_lock():
|
|
67
|
+
"""Exclusive advisory lock guarding the marker read-modify-write.
|
|
68
|
+
|
|
69
|
+
Two hook processes warning for different sessions at the same time must
|
|
70
|
+
not read the same old key set and write back over each other. The lock
|
|
71
|
+
lives on a dedicated sidecar file that the marker write never replaces,
|
|
72
|
+
so the locked inode cannot be swapped out mid-section, and the kernel
|
|
73
|
+
drops an ``flock`` when its holder exits or crashes, so a dead hook
|
|
74
|
+
process cannot wedge later sessions. On platforms without ``fcntl``, or
|
|
75
|
+
when the lock file cannot be created, this degrades to the historical
|
|
76
|
+
unlocked best-effort behaviour instead of failing the warning.
|
|
77
|
+
"""
|
|
78
|
+
if fcntl is None:
|
|
79
|
+
yield
|
|
80
|
+
return
|
|
81
|
+
lock_path = _NO_TOOLS_WARN_MARKER + ".lock"
|
|
82
|
+
fd = -1
|
|
83
|
+
try:
|
|
84
|
+
os.makedirs(os.path.dirname(lock_path), mode=0o700, exist_ok=True)
|
|
85
|
+
if os.path.islink(lock_path):
|
|
86
|
+
os.unlink(lock_path)
|
|
87
|
+
flags = os.O_RDWR | os.O_CREAT
|
|
88
|
+
if hasattr(os, "O_NOFOLLOW"):
|
|
89
|
+
flags |= os.O_NOFOLLOW
|
|
90
|
+
fd = os.open(lock_path, flags, 0o600)
|
|
91
|
+
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
92
|
+
except OSError:
|
|
93
|
+
if fd >= 0:
|
|
94
|
+
os.close(fd)
|
|
95
|
+
yield # lock unavailable — keep the unlocked best-effort behaviour
|
|
96
|
+
return
|
|
97
|
+
try:
|
|
98
|
+
yield
|
|
99
|
+
finally:
|
|
100
|
+
os.close(fd)
|
|
101
|
+
|
|
36
102
|
|
|
37
103
|
# -- helpers -----------------------------------------------------------------
|
|
38
104
|
|
|
@@ -45,6 +111,100 @@ def _is_json_array(data: str) -> bool:
|
|
|
45
111
|
return False
|
|
46
112
|
|
|
47
113
|
|
|
114
|
+
def _session_warn_key(session_id: str) -> str:
|
|
115
|
+
"""Normalize the session ID into the dedup key for the no-tools warning.
|
|
116
|
+
|
|
117
|
+
Hosts that omit a session ID share one key, so the warning still appears
|
|
118
|
+
at most once for them instead of on every turn.
|
|
119
|
+
"""
|
|
120
|
+
key = (session_id or "").strip()
|
|
121
|
+
return key or "<no-session>"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_warned_sessions() -> list:
|
|
125
|
+
"""Read the warned session keys (one per line) from the marker file.
|
|
126
|
+
|
|
127
|
+
Tolerates a missing or corrupt marker — dedup is best-effort and a miss
|
|
128
|
+
simply means the session warns once (again).
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
with open(_NO_TOOLS_WARN_MARKER, "r", encoding="utf-8") as handle:
|
|
132
|
+
return [line.strip() for line in handle if line.strip()]
|
|
133
|
+
except OSError:
|
|
134
|
+
return [] # first invocation, or unreadable marker
|
|
135
|
+
except UnicodeDecodeError:
|
|
136
|
+
return [] # corrupt marker — rebuild it on the next warning
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _should_warn_no_tools(session_id: str) -> bool:
|
|
140
|
+
"""Return True if the no-tools warning has not been emitted for this
|
|
141
|
+
session yet, and record that it is being emitted now.
|
|
142
|
+
|
|
143
|
+
Every warned session key stays in the marker (bounded to the newest
|
|
144
|
+
``_MAX_WARNED_SESSIONS`` entries), so concurrent sessions under one HOME
|
|
145
|
+
do not invalidate each other's dedup state. The check-and-record runs
|
|
146
|
+
inside ``_marker_lock`` so two first warnings racing for different
|
|
147
|
+
sessions serialize instead of overwriting each other's keys. Best-effort
|
|
148
|
+
by design: the marker lives under ``~/.tokenless`` and is read/written
|
|
149
|
+
with the same hardened helpers as other hook state, but any failure
|
|
150
|
+
falls back to warning — observability beats silence, and the warning
|
|
151
|
+
itself never affects the pass-through behaviour.
|
|
152
|
+
"""
|
|
153
|
+
key = _session_warn_key(session_id)
|
|
154
|
+
with _marker_lock():
|
|
155
|
+
warned = _read_warned_sessions()
|
|
156
|
+
if key in warned:
|
|
157
|
+
return False
|
|
158
|
+
warned.append(key)
|
|
159
|
+
warned = warned[-_MAX_WARNED_SESSIONS:]
|
|
160
|
+
try:
|
|
161
|
+
secure_write_text(
|
|
162
|
+
_NO_TOOLS_WARN_MARKER, "".join(entry + "\n" for entry in warned)
|
|
163
|
+
)
|
|
164
|
+
except OSError:
|
|
165
|
+
pass # state dir unwritable — still warn this once
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _skip_with_message(msg: str) -> None:
|
|
170
|
+
"""Pass through unchanged while surfacing ``msg`` to the user.
|
|
171
|
+
|
|
172
|
+
``systemMessage`` is the protocol field both hosts surface for humans:
|
|
173
|
+
Cosh-NG renders it as a hook notification and copilot-shell records it in
|
|
174
|
+
its hook log, while neither injects it into the model context. The
|
|
175
|
+
warning is also printed to stderr so hosts that forward hook stderr get
|
|
176
|
+
the same text.
|
|
177
|
+
"""
|
|
178
|
+
warn(msg)
|
|
179
|
+
print(json.dumps({"systemMessage": msg}))
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_tools(input_data: dict) -> tuple[object, bool]:
|
|
184
|
+
"""Extract the tool declarations from a BeforeModel payload.
|
|
185
|
+
|
|
186
|
+
Returns ``(tools, declared)`` where ``declared`` is True when the host
|
|
187
|
+
carried a tools field at any known position — even an empty one.
|
|
188
|
+
|
|
189
|
+
``config.tools`` is the canonical position (both copilot-shell's Hook
|
|
190
|
+
Translator and Cosh-NG put it there); the top-level ``tools`` is the
|
|
191
|
+
older position, kept for hosts that still emit it. Presence of the
|
|
192
|
+
canonical key decides, not its truthiness: a host that declares no tools
|
|
193
|
+
sends an empty canonical array, and falling through to a stale legacy
|
|
194
|
+
field there would compress declarations this request never carried. This
|
|
195
|
+
mirrors the host-side precedence.
|
|
196
|
+
"""
|
|
197
|
+
llm_request = input_data.get("llm_request")
|
|
198
|
+
if not isinstance(llm_request, dict):
|
|
199
|
+
return None, False
|
|
200
|
+
config = llm_request.get("config")
|
|
201
|
+
if isinstance(config, dict) and "tools" in config:
|
|
202
|
+
return config["tools"], True
|
|
203
|
+
if "tools" in llm_request:
|
|
204
|
+
return llm_request.get("tools"), True
|
|
205
|
+
return None, False
|
|
206
|
+
|
|
207
|
+
|
|
48
208
|
# -- main --------------------------------------------------------------------
|
|
49
209
|
|
|
50
210
|
|
|
@@ -68,21 +228,42 @@ def main() -> None:
|
|
|
68
228
|
except (json.JSONDecodeError, EOFError, ValueError):
|
|
69
229
|
warn("failed to read BeforeModel payload. Passing through unchanged.")
|
|
70
230
|
skip()
|
|
231
|
+
if not isinstance(input_data, dict):
|
|
232
|
+
# No session_id available outside a dict payload — the shared
|
|
233
|
+
# "<no-session>" key still bounds this to one warning.
|
|
234
|
+
if _should_warn_no_tools(""):
|
|
235
|
+
_skip_with_message(
|
|
236
|
+
"BeforeModel payload is not a JSON object. Passing through "
|
|
237
|
+
"unchanged (warned once per session)."
|
|
238
|
+
)
|
|
239
|
+
skip()
|
|
71
240
|
|
|
72
|
-
# 3. Extract tools array.
|
|
73
|
-
|
|
74
|
-
# `tools` is the older position, kept for hosts that still emit it.
|
|
75
|
-
# Presence of the canonical key decides, not its truthiness: a host that
|
|
76
|
-
# declares no tools sends an empty canonical array, and falling through to
|
|
77
|
-
# a stale legacy field there would compress declarations this request never
|
|
78
|
-
# carried. This mirrors the host-side precedence.
|
|
79
|
-
llm_request = input_data.get("llm_request", {})
|
|
80
|
-
config = llm_request.get("config")
|
|
81
|
-
if isinstance(config, dict) and "tools" in config:
|
|
82
|
-
tools = config["tools"]
|
|
83
|
-
else:
|
|
84
|
-
tools = llm_request.get("tools")
|
|
241
|
+
# 3. Extract tools array.
|
|
242
|
+
tools, tools_declared = _extract_tools(input_data)
|
|
85
243
|
if not tools:
|
|
244
|
+
# A host that declares no tools for this turn (empty canonical array)
|
|
245
|
+
# skips silently — that is a normal request. But a BeforeModel event
|
|
246
|
+
# with no tools field at any known position means there is nothing
|
|
247
|
+
# for schema compression to work on. That skip used to be silent, so
|
|
248
|
+
# "0 schema records" could not be told apart from "hook never ran";
|
|
249
|
+
# warn once per session instead.
|
|
250
|
+
if not tools_declared and _should_warn_no_tools(
|
|
251
|
+
str(input_data.get("session_id", ""))
|
|
252
|
+
):
|
|
253
|
+
if not isinstance(input_data.get("llm_request"), dict):
|
|
254
|
+
_skip_with_message(
|
|
255
|
+
"BeforeModel payload carries no llm_request object, so schema "
|
|
256
|
+
"compression cannot find tool declarations. Passing through "
|
|
257
|
+
"unchanged (warned once per session)."
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
_skip_with_message(
|
|
261
|
+
"BeforeModel event carries no tool declarations at "
|
|
262
|
+
"llm_request.config.tools or llm_request.tools, so schema "
|
|
263
|
+
"compression was skipped. If the host declares tools, its "
|
|
264
|
+
"event format may not match the hook. Passing through "
|
|
265
|
+
"unchanged (warned once per session)."
|
|
266
|
+
)
|
|
86
267
|
skip()
|
|
87
268
|
|
|
88
269
|
tools_json = json.dumps(tools, separators=(",", ":"))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "tokenless",
|
|
3
3
|
"name": "Tokenless",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.12",
|
|
5
5
|
"description": "Unified RTK command rewriting + response/TOON compression + registered but hard-disabled Tool Ready. Wraps tokenless and rtk system binaries via child_process — this is expected and not malicious.",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": ["hook"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenless/openclaw-plugin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Unified OpenClaw plugin — RTK command rewriting + tokenless schema/response compression for 60-90% LLM token savings",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenless",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
4
4
|
"description": "Token-Less context compression for Qwen Code — RTK command rewriting, response/TOON/schema compression, and registered but hard-disabled Tool Ready",
|
|
5
5
|
"author": { "name": "ANOLISA" },
|
|
6
6
|
"license": "Apache-2.0",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anolisa-tokenless",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.12",
|
|
5
5
|
"description": "Token-Less — LLM token optimization toolkit (schema/response compression, command rewriting, tool readiness)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"arm64"
|
|
45
45
|
],
|
|
46
46
|
"optionalDependencies": {
|
|
47
|
-
"@anolisa/tokenless-linux-x64": "0.7.
|
|
48
|
-
"@anolisa/tokenless-linux-arm64": "0.7.
|
|
49
|
-
"@anolisa/tokenless-darwin-x64": "0.7.
|
|
50
|
-
"@anolisa/tokenless-darwin-arm64": "0.7.
|
|
47
|
+
"@anolisa/tokenless-linux-x64": "0.7.12",
|
|
48
|
+
"@anolisa/tokenless-linux-arm64": "0.7.12",
|
|
49
|
+
"@anolisa/tokenless-darwin-x64": "0.7.12",
|
|
50
|
+
"@anolisa/tokenless-darwin-arm64": "0.7.12"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"registry": "https://registry.npmjs.org/",
|