datoon 2.0.0__tar.gz → 3.0.0__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.
- {datoon-2.0.0 → datoon-3.0.0}/.gitignore +1 -0
- {datoon-2.0.0 → datoon-3.0.0}/CHANGELOG.md +19 -0
- {datoon-2.0.0 → datoon-3.0.0}/CLAUDE.md +4 -1
- {datoon-2.0.0 → datoon-3.0.0}/INSTALL.md +20 -0
- {datoon-2.0.0 → datoon-3.0.0}/PKG-INFO +27 -3
- {datoon-2.0.0 → datoon-3.0.0}/README.md +21 -1
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/generate_payloads.py +3 -6
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/run.py +17 -5
- {datoon-2.0.0 → datoon-3.0.0}/pyproject.toml +3 -1
- {datoon-2.0.0 → datoon-3.0.0}/scripts/ab_skill_benchmark.py +3 -4
- {datoon-2.0.0 → datoon-3.0.0}/scripts/install.py +3 -4
- {datoon-2.0.0 → datoon-3.0.0}/scripts/summarize_agent_skill_eval.py +0 -1
- {datoon-2.0.0 → datoon-3.0.0}/scripts/validate_plugin_metadata.py +1 -2
- {datoon-2.0.0 → datoon-3.0.0}/scripts/validate_skill_sync.py +1 -3
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/__init__.py +9 -5
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/_version.py +3 -3
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/cli.py +13 -6
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/converter.py +16 -26
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/mcp_server.py +39 -5
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/models.py +9 -7
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/columnar.py +1 -1
- datoon-3.0.0/src/datoon/tokenizers.py +298 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/PKG-INFO +27 -3
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/SOURCES.txt +3 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/requires.txt +7 -1
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/scm_file_list.json +3 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/scm_version.json +2 -2
- datoon-3.0.0/tests/conftest.py +11 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_cli.py +35 -10
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_converter_policy.py +42 -2
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_errors.py +1 -1
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_mcp_server.py +42 -1
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_models.py +15 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_readers.py +2 -10
- datoon-3.0.0/tests/unit/test_tokenizers.py +366 -0
- datoon-3.0.0/tests/unit/test_tokens.py +75 -0
- {datoon-2.0.0 → datoon-3.0.0}/uv.lock +249 -8
- datoon-2.0.0/tests/unit/test_tokens.py +0 -43
- {datoon-2.0.0 → datoon-3.0.0}/.agents/plugins/marketplace.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.claude-plugin/marketplace.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.claude-plugin/plugin.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.editorconfig +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.gitattributes +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/dependabot.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/package-installer-smoke.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/pages.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/pre-commit.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/publish-mcp.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/publish.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/release.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/tests.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/validate-artifacts.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/workflow-lint.yml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.pre-commit-config.yaml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/.releaserc.yaml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/CONTRIBUTING.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/LICENSE +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/MARKETPLACES.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/SECURITY.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/SKILL.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/assets/datoon-banner.svg +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/payloads.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/benchmarks/results/.gitkeep +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/datoon/SKILL.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/datoon.skill +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/docs/.nojekyll +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/docs/app.js +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/docs/assets/datoon-banner.svg +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/docs/index.html +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/docs/styles.css +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/glama.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/install.sh +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/server.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/setup.cfg +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/setup.sh +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/skills/datoon/README.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/skills/datoon/SKILL.md +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/smithery.yaml +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/analyzer.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/errors.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/py.typed +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/__init__.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/_coerce.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/_tabular.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/csv.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/excel.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/jsonl.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/numbers.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/xml.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/yaml.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/dependency_links.txt +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/entry_points.txt +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/top_level.txt +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/fixtures/users.expected.toon +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/fixtures/users.json +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/integration/test_toon_roundtrip.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_analyzer.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_benchmark_readme.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_installer.py +0 -0
- {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_tabular_rows.py +0 -0
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [3.0.0](https://github.com/andrii-su/datoon/compare/v2.0.0...v3.0.0) (2026-11-11)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
* feat!: selectable token counters (auto, chars, tiktoken, claude, hf, callable) ([cab2bfb](https://github.com/andrii-su/datoon/commit/cab2bfb9f1106f2a4cf40122588249acb0371f94))
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **mcp:** accept only local token counters as a tool argument ([1f2e54d](https://github.com/andrii-su/datoon/commit/1f2e54d4f83ea266590933f0762019d2d37f7270))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### BREAKING CHANGES
|
|
13
|
+
|
|
14
|
+
* `ConversionConfig.token_encoding` is replaced by
|
|
15
|
+
`tokenizer`; the CLI flag `--encoding` and the MCP `encoding` argument are
|
|
16
|
+
replaced by `--tokenizer` / `tokenizer` (default from `DATOON_TOKENIZER`,
|
|
17
|
+
else `auto`). `ConversionReport` gains a `tokenizer` field. Every counter
|
|
18
|
+
returns at least 1, including for empty text.
|
|
19
|
+
|
|
1
20
|
# [2.0.0](https://github.com/andrii-su/datoon/compare/v1.9.1...v2.0.0) (2026-11-11)
|
|
2
21
|
|
|
3
22
|
|
|
@@ -32,6 +32,7 @@ datoon/
|
|
|
32
32
|
│ ├── analyzer.py # Payload structure suitability checks
|
|
33
33
|
│ ├── converter.py # JSON normalization, TOON encoder call, policy gating
|
|
34
34
|
│ ├── models.py # Config/report/outcome dataclasses
|
|
35
|
+
│ ├── tokenizers.py # Token counter specs and estimates
|
|
35
36
|
│ ├── cli.py # CLI entrypoint
|
|
36
37
|
│ └── mcp_server.py # MCP server tools
|
|
37
38
|
│
|
|
@@ -68,6 +69,7 @@ Edit these files directly:
|
|
|
68
69
|
| Conversion policy | `src/datoon/converter.py` |
|
|
69
70
|
| Payload analysis | `src/datoon/analyzer.py` |
|
|
70
71
|
| Public data contracts | `src/datoon/models.py` |
|
|
72
|
+
| Token counters | `src/datoon/tokenizers.py` |
|
|
71
73
|
| CLI behavior | `src/datoon/cli.py` |
|
|
72
74
|
| MCP behavior | `src/datoon/mcp_server.py` |
|
|
73
75
|
| Skill behavior | `skills/datoon/SKILL.md` |
|
|
@@ -109,7 +111,8 @@ It runs on pull requests and pushes to `main`. The old auto-sync-on-main flow wa
|
|
|
109
111
|
- `--force` is for controlled experiments and should bypass gating, not hide conversion errors.
|
|
110
112
|
- Auto mode skips before conversion when analysis rejects the payload, and skips after conversion when estimated savings are below `min_savings_ratio`.
|
|
111
113
|
- Keep skip reasons user-facing, specific, and stable enough for CLI/MCP troubleshooting.
|
|
112
|
-
- Token counts are estimates. Be precise about whether a number is payload-token estimate, visible output estimate, or real model usage.
|
|
114
|
+
- Token counts are estimates, whichever tokenizer produced them (`claude:` counts included). Be precise about whether a number is payload-token estimate, visible output estimate, or real model usage, and name the tokenizer it came from.
|
|
115
|
+
- Only the `auto` tokenizer falls back silently; an explicit spec with a missing package or credentials must raise `DatoonError`, in auto and force mode alike.
|
|
113
116
|
- Benchmarks in README must come from committed scripts/artifacts.
|
|
114
117
|
- Keep generated caches out of commits: `.venv/`, `.pytest_cache/`, `.ruff_cache/`, `__pycache__/`, local benchmark result JSON under `benchmarks/results/`.
|
|
115
118
|
|
|
@@ -42,9 +42,29 @@ Optional extras:
|
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
pip install "datoon[tokens]" # tiktoken token estimates
|
|
45
|
+
pip install "datoon[claude]" # Claude token counts via the Anthropic API
|
|
46
|
+
pip install "datoon[hf]" # Hugging Face tokenizers
|
|
45
47
|
pip install "datoon[mcp]" # MCP server dependencies
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
### Choosing a tokenizer
|
|
51
|
+
|
|
52
|
+
Select the token counter with `--tokenizer <spec>` (CLI), the `tokenizer` argument (MCP tools), or `DATOON_TOKENIZER`; the default is `auto`. The MCP tools accept only local counters (`auto`, `chars`, `tiktoken`) per call; `claude:` and `hf:` reach the network, so only the server operator can enable them through `DATOON_TOKENIZER`.
|
|
53
|
+
|
|
54
|
+
| Spec | Counts with | Needs |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `auto` | `tiktoken:o200k_base` when tiktoken is installed, else `chars:3` | — |
|
|
57
|
+
| `chars` / `chars:<N>` | `ceil(len(text) / N)`, default `N = 3` | — |
|
|
58
|
+
| `tiktoken` / `tiktoken:<encoding>` | tiktoken, default `o200k_base` | `datoon[tokens]` |
|
|
59
|
+
| `claude:<model-id>` | Anthropic token counting API | `datoon[claude]`, Anthropic credentials (e.g. `ANTHROPIC_API_KEY`) |
|
|
60
|
+
| `hf:<repo-id>` | Hugging Face `tokenizers` tokenizer | `datoon[hf]` |
|
|
61
|
+
|
|
62
|
+
Local counters suit runtime gating; `claude:` and `hf:` suit benchmarks and evals where counts for the target model matter. `claude:` makes network calls, and Anthropic documents its counts as estimates. `hf:` downloads the tokenizer from the Hugging Face Hub on first use.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
datoon input.json --tokenizer claude:claude-opus-5-5 --report-stdout
|
|
66
|
+
```
|
|
67
|
+
|
|
48
68
|
## CLI
|
|
49
69
|
|
|
50
70
|
Read from stdin:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datoon
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Smart structured-data-to-TOON gateway with pragmatic auto-gating for LLM prompts.
|
|
5
5
|
Author-email: Andrii Suruhov <andrii.suruhov@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -44,6 +44,10 @@ Requires-Dist: pre-commit>=4.6.0; extra == "dev"
|
|
|
44
44
|
Requires-Dist: pyyaml>=6.0; extra == "dev"
|
|
45
45
|
Provides-Extra: tokens
|
|
46
46
|
Requires-Dist: tiktoken>=0.13.0; extra == "tokens"
|
|
47
|
+
Provides-Extra: claude
|
|
48
|
+
Requires-Dist: anthropic>=2.0.0; extra == "claude"
|
|
49
|
+
Provides-Extra: hf
|
|
50
|
+
Requires-Dist: tokenizers>=0.23.0; extra == "hf"
|
|
47
51
|
Provides-Extra: mcp
|
|
48
52
|
Requires-Dist: mcp>=1.28.0; extra == "mcp"
|
|
49
53
|
Provides-Extra: yaml
|
|
@@ -56,7 +60,7 @@ Requires-Dist: fastavro>=1.9; extra == "columnar"
|
|
|
56
60
|
Provides-Extra: numbers
|
|
57
61
|
Requires-Dist: numbers-parser>=4.4; extra == "numbers"
|
|
58
62
|
Provides-Extra: all
|
|
59
|
-
Requires-Dist: datoon[columnar,excel,mcp,numbers,tokens,yaml]; extra == "all"
|
|
63
|
+
Requires-Dist: datoon[claude,columnar,excel,hf,mcp,numbers,tokens,yaml]; extra == "all"
|
|
60
64
|
Dynamic: license-file
|
|
61
65
|
|
|
62
66
|
<p align="center">
|
|
@@ -212,6 +216,10 @@ pip install "datoon[numbers]"
|
|
|
212
216
|
# with tiktoken-based token counting
|
|
213
217
|
pip install "datoon[tokens]"
|
|
214
218
|
|
|
219
|
+
# with Claude token counting (Anthropic API) / Hugging Face tokenizers
|
|
220
|
+
pip install "datoon[claude]"
|
|
221
|
+
pip install "datoon[hf]"
|
|
222
|
+
|
|
215
223
|
# with MCP server
|
|
216
224
|
pip install "datoon[mcp]"
|
|
217
225
|
|
|
@@ -257,7 +265,22 @@ For Claude Code plugin, Codex, and MCP config → [**INSTALL.md**](./INSTALL.md)
|
|
|
257
265
|
1. **Convert + estimate** — TOON encoder runs in-process, token savings calculated
|
|
258
266
|
1. **Gate savings** — below threshold → return JSON; above → return TOON with report
|
|
259
267
|
|
|
260
|
-
Every path returns a `ConversionReport` with `decision`, `reason`, and
|
|
268
|
+
Every path returns a `ConversionReport` with `decision`, `reason`, token estimates, and the `tokenizer` that produced them. Pipelines never get silent surprises.
|
|
269
|
+
|
|
270
|
+
## Choosing a tokenizer
|
|
271
|
+
|
|
272
|
+
Token estimates drive the savings gate, so pick the counter that fits the job. Pass one spec with `--tokenizer`, the `tokenizer` MCP argument, `ConversionConfig(tokenizer=...)`, or the `DATOON_TOKENIZER` environment variable; in Python any callable `str -> int` works too. The MCP tools accept only local counters (`auto`, `chars`, `tiktoken`) per call; `claude:` and `hf:` reach the network, so only the server operator can enable them through `DATOON_TOKENIZER`.
|
|
273
|
+
|
|
274
|
+
| Spec | Counts with | Needs |
|
|
275
|
+
|---|---|---|
|
|
276
|
+
| `auto` (default) | `tiktoken:o200k_base` when tiktoken is installed, else `chars:3` | — |
|
|
277
|
+
| `chars` / `chars:<N>` | `ceil(len(text) / N)`, default `N = 3` | — |
|
|
278
|
+
| `tiktoken` / `tiktoken:<encoding>` | tiktoken, default `o200k_base` | `datoon[tokens]` |
|
|
279
|
+
| `claude:<model-id>` | Anthropic token counting API for that model | `datoon[claude]`, Anthropic credentials (e.g. `ANTHROPIC_API_KEY`) |
|
|
280
|
+
| `hf:<repo-id>` | Hugging Face `tokenizers` tokenizer from the Hub | `datoon[hf]` |
|
|
281
|
+
| a callable (Python) | your own function, reported as `custom` | — |
|
|
282
|
+
|
|
283
|
+
Use the local counters (`auto`, `chars`, `tiktoken`) to gate payloads at runtime: they are fast and run locally (tiktoken fetches its encoding file once on first use). Use `claude:` or `hf:` in benchmarks and evals where counts for the target model matter. `claude:` makes network calls, and Anthropic documents its counts as estimates. Only `auto` falls back silently: an explicit spec with a missing package fails with an error that names the extra to install, and `claude:` without Anthropic credentials fails too. Every report records the resolved name in `tokenizer`.
|
|
261
284
|
|
|
262
285
|
______________________________________________________________________
|
|
263
286
|
|
|
@@ -420,6 +443,7 @@ ______________________________________________________________________
|
|
|
420
443
|
| `--min-savings` | `0.15` | Minimum relative token savings required |
|
|
421
444
|
| `--max-depth` | `6` | Maximum nesting depth for auto-conversion |
|
|
422
445
|
| `--min-uniform-rows` | `3` | Minimum rows in uniform object arrays |
|
|
446
|
+
| `--tokenizer <spec>` | `$DATOON_TOKENIZER`, else `auto` | Token counter for estimates — see [Choosing a tokenizer](#choosing-a-tokenizer) |
|
|
423
447
|
| `--report <path>` | — | Write JSON conversion report to file |
|
|
424
448
|
| `--report-stdout` | — | Print JSON conversion report to stderr |
|
|
425
449
|
| `-o <path>` | stdout | Output file path |
|
|
@@ -151,6 +151,10 @@ pip install "datoon[numbers]"
|
|
|
151
151
|
# with tiktoken-based token counting
|
|
152
152
|
pip install "datoon[tokens]"
|
|
153
153
|
|
|
154
|
+
# with Claude token counting (Anthropic API) / Hugging Face tokenizers
|
|
155
|
+
pip install "datoon[claude]"
|
|
156
|
+
pip install "datoon[hf]"
|
|
157
|
+
|
|
154
158
|
# with MCP server
|
|
155
159
|
pip install "datoon[mcp]"
|
|
156
160
|
|
|
@@ -196,7 +200,22 @@ For Claude Code plugin, Codex, and MCP config → [**INSTALL.md**](./INSTALL.md)
|
|
|
196
200
|
1. **Convert + estimate** — TOON encoder runs in-process, token savings calculated
|
|
197
201
|
1. **Gate savings** — below threshold → return JSON; above → return TOON with report
|
|
198
202
|
|
|
199
|
-
Every path returns a `ConversionReport` with `decision`, `reason`, and
|
|
203
|
+
Every path returns a `ConversionReport` with `decision`, `reason`, token estimates, and the `tokenizer` that produced them. Pipelines never get silent surprises.
|
|
204
|
+
|
|
205
|
+
## Choosing a tokenizer
|
|
206
|
+
|
|
207
|
+
Token estimates drive the savings gate, so pick the counter that fits the job. Pass one spec with `--tokenizer`, the `tokenizer` MCP argument, `ConversionConfig(tokenizer=...)`, or the `DATOON_TOKENIZER` environment variable; in Python any callable `str -> int` works too. The MCP tools accept only local counters (`auto`, `chars`, `tiktoken`) per call; `claude:` and `hf:` reach the network, so only the server operator can enable them through `DATOON_TOKENIZER`.
|
|
208
|
+
|
|
209
|
+
| Spec | Counts with | Needs |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `auto` (default) | `tiktoken:o200k_base` when tiktoken is installed, else `chars:3` | — |
|
|
212
|
+
| `chars` / `chars:<N>` | `ceil(len(text) / N)`, default `N = 3` | — |
|
|
213
|
+
| `tiktoken` / `tiktoken:<encoding>` | tiktoken, default `o200k_base` | `datoon[tokens]` |
|
|
214
|
+
| `claude:<model-id>` | Anthropic token counting API for that model | `datoon[claude]`, Anthropic credentials (e.g. `ANTHROPIC_API_KEY`) |
|
|
215
|
+
| `hf:<repo-id>` | Hugging Face `tokenizers` tokenizer from the Hub | `datoon[hf]` |
|
|
216
|
+
| a callable (Python) | your own function, reported as `custom` | — |
|
|
217
|
+
|
|
218
|
+
Use the local counters (`auto`, `chars`, `tiktoken`) to gate payloads at runtime: they are fast and run locally (tiktoken fetches its encoding file once on first use). Use `claude:` or `hf:` in benchmarks and evals where counts for the target model matter. `claude:` makes network calls, and Anthropic documents its counts as estimates. Only `auto` falls back silently: an explicit spec with a missing package fails with an error that names the extra to install, and `claude:` without Anthropic credentials fails too. Every report records the resolved name in `tokenizer`.
|
|
200
219
|
|
|
201
220
|
______________________________________________________________________
|
|
202
221
|
|
|
@@ -359,6 +378,7 @@ ______________________________________________________________________
|
|
|
359
378
|
| `--min-savings` | `0.15` | Minimum relative token savings required |
|
|
360
379
|
| `--max-depth` | `6` | Maximum nesting depth for auto-conversion |
|
|
361
380
|
| `--min-uniform-rows` | `3` | Minimum rows in uniform object arrays |
|
|
381
|
+
| `--tokenizer <spec>` | `$DATOON_TOKENIZER`, else `auto` | Token counter for estimates — see [Choosing a tokenizer](#choosing-a-tokenizer) |
|
|
362
382
|
| `--report <path>` | — | Write JSON conversion report to file |
|
|
363
383
|
| `--report-stdout` | — | Print JSON conversion report to stderr |
|
|
364
384
|
| `-o <path>` | stdout | Output file path |
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Generate deterministic JSON payloads for agent skill evaluation."""
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
@@ -67,12 +66,10 @@ def expected_for(payload: dict[str, object]) -> dict[str, object]:
|
|
|
67
66
|
region_totals[region] = region_totals.get(region, 0) + revenue
|
|
68
67
|
category_totals[category] = category_totals.get(category, 0) + revenue
|
|
69
68
|
|
|
70
|
-
top_region =
|
|
69
|
+
top_region = min(region_totals.items(), key=lambda item: (-item[1], item[0]))[0][0]
|
|
70
|
+
top_category = min(category_totals.items(), key=lambda item: (-item[1], item[0]))[
|
|
71
71
|
0
|
|
72
|
-
]
|
|
73
|
-
top_category = sorted(
|
|
74
|
-
category_totals.items(), key=lambda item: (-item[1], item[0])
|
|
75
|
-
)[0][0]
|
|
72
|
+
][0]
|
|
76
73
|
|
|
77
74
|
return {
|
|
78
75
|
"scenario": payload["scenario"],
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Benchmark JSON vs TOON efficiency and datoon auto-conversion decisions."""
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
@@ -9,7 +8,7 @@ import json
|
|
|
9
8
|
import statistics
|
|
10
9
|
import sys
|
|
11
10
|
from dataclasses import asdict, dataclass
|
|
12
|
-
from datetime import
|
|
11
|
+
from datetime import UTC, datetime
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
from typing import Any
|
|
15
14
|
|
|
@@ -126,7 +125,7 @@ def benchmark_payload(
|
|
|
126
125
|
raw_error=str(exc),
|
|
127
126
|
)
|
|
128
127
|
|
|
129
|
-
json_tokens = estimate_tokens(raw_json)
|
|
128
|
+
json_tokens = estimate_tokens(raw_json, auto_config.tokenizer)
|
|
130
129
|
|
|
131
130
|
toon_tokens: int | None = None
|
|
132
131
|
raw_savings_pct: float | None = None
|
|
@@ -248,16 +247,17 @@ def save_results(
|
|
|
248
247
|
) -> Path:
|
|
249
248
|
"""Save benchmark output to versioned JSON artifact."""
|
|
250
249
|
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
251
|
-
timestamp = datetime.now(
|
|
250
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
|
|
252
251
|
output = {
|
|
253
252
|
"metadata": {
|
|
254
253
|
"script_version": SCRIPT_VERSION,
|
|
255
|
-
"generated_at": datetime.now(
|
|
254
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
256
255
|
"payloads_sha256": payloads_sha,
|
|
257
256
|
"config": {
|
|
258
257
|
"min_savings_ratio": config.min_savings_ratio,
|
|
259
258
|
"max_depth": config.max_depth,
|
|
260
259
|
"min_uniform_rows": config.min_uniform_rows,
|
|
260
|
+
"tokenizer": config.tokenizer,
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
263
|
"summary": asdict(summary),
|
|
@@ -348,6 +348,7 @@ def dry_run(
|
|
|
348
348
|
print(f"- min_savings_ratio: {config.min_savings_ratio}")
|
|
349
349
|
print(f"- max_depth: {config.max_depth}")
|
|
350
350
|
print(f"- min_uniform_rows: {config.min_uniform_rows}")
|
|
351
|
+
print(f"- tokenizer: {config.tokenizer}")
|
|
351
352
|
print(f"- json payloads: {len(payloads)}")
|
|
352
353
|
for payload in payloads:
|
|
353
354
|
print(
|
|
@@ -384,6 +385,15 @@ def parse_args() -> argparse.Namespace:
|
|
|
384
385
|
default=3,
|
|
385
386
|
help="Auto mode minimum rows for uniform object arrays.",
|
|
386
387
|
)
|
|
388
|
+
parser.add_argument(
|
|
389
|
+
"--tokenizer",
|
|
390
|
+
default="auto",
|
|
391
|
+
metavar="SPEC",
|
|
392
|
+
help=(
|
|
393
|
+
"Token counter for estimates: auto, chars[:N], tiktoken[:ENCODING], "
|
|
394
|
+
"claude:MODEL_ID (network) or hf:REPO_ID (default: auto)."
|
|
395
|
+
),
|
|
396
|
+
)
|
|
387
397
|
parser.add_argument(
|
|
388
398
|
"--dry-run",
|
|
389
399
|
action="store_true",
|
|
@@ -413,12 +423,14 @@ def main() -> int:
|
|
|
413
423
|
max_depth=args.max_depth,
|
|
414
424
|
min_uniform_rows=args.min_uniform_rows,
|
|
415
425
|
force=False,
|
|
426
|
+
tokenizer=args.tokenizer,
|
|
416
427
|
)
|
|
417
428
|
force_config = ConversionConfig(
|
|
418
429
|
min_savings_ratio=0.0,
|
|
419
430
|
max_depth=max(64, args.max_depth),
|
|
420
431
|
min_uniform_rows=2,
|
|
421
432
|
force=True,
|
|
433
|
+
tokenizer=args.tokenizer,
|
|
422
434
|
)
|
|
423
435
|
|
|
424
436
|
if args.dry_run:
|
|
@@ -27,12 +27,14 @@ dependencies = [
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
28
|
dev = ["pytest>=9.0.3", "pre-commit>=4.6.0", "pyyaml>=6.0"]
|
|
29
29
|
tokens = ["tiktoken>=0.13.0"]
|
|
30
|
+
claude = ["anthropic>=2.0.0"]
|
|
31
|
+
hf = ["tokenizers>=0.23.0"]
|
|
30
32
|
mcp = ["mcp>=1.28.0"]
|
|
31
33
|
yaml = ["pyyaml>=6.0"]
|
|
32
34
|
excel = ["openpyxl>=3.1"]
|
|
33
35
|
columnar = ["pyarrow>=15.0", "fastavro>=1.9"]
|
|
34
36
|
numbers = ["numbers-parser>=4.4"]
|
|
35
|
-
all = ["datoon[tokens,mcp,yaml,excel,columnar,numbers]"]
|
|
37
|
+
all = ["datoon[tokens,claude,hf,mcp,yaml,excel,columnar,numbers]"]
|
|
36
38
|
|
|
37
39
|
[project.urls]
|
|
38
40
|
Homepage = "https://github.com/andrii-su/datoon"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""A/B benchmark for datoon skill usage in Claude CLI.
|
|
3
2
|
|
|
4
3
|
This script compares the same prompts in two modes:
|
|
@@ -18,7 +17,7 @@ import math
|
|
|
18
17
|
import statistics
|
|
19
18
|
import subprocess
|
|
20
19
|
from dataclasses import asdict, dataclass
|
|
21
|
-
from datetime import
|
|
20
|
+
from datetime import UTC, datetime
|
|
22
21
|
from pathlib import Path
|
|
23
22
|
from typing import Any
|
|
24
23
|
|
|
@@ -427,7 +426,7 @@ def main() -> int:
|
|
|
427
426
|
if args.payload_limit < 1:
|
|
428
427
|
raise ValueError("--payload-limit must be >= 1.")
|
|
429
428
|
|
|
430
|
-
timestamp = datetime.now(
|
|
429
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S")
|
|
431
430
|
log_path = args.output_dir / "logs" / f"ab_skill_{timestamp}.log"
|
|
432
431
|
logger = configure_logging(log_path, verbose=args.verbose)
|
|
433
432
|
|
|
@@ -606,7 +605,7 @@ def main() -> int:
|
|
|
606
605
|
|
|
607
606
|
output = {
|
|
608
607
|
"metadata": {
|
|
609
|
-
"generated_at": datetime.now(
|
|
608
|
+
"generated_at": datetime.now(UTC).isoformat(),
|
|
610
609
|
"payload_ids": payload_ids,
|
|
611
610
|
"iterations": args.iterations,
|
|
612
611
|
"model": args.model,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Install, dry-run, or uninstall datoon integrations."""
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
@@ -10,7 +9,7 @@ import shutil
|
|
|
10
9
|
import subprocess
|
|
11
10
|
import sys
|
|
12
11
|
from dataclasses import dataclass
|
|
13
|
-
from datetime import
|
|
12
|
+
from datetime import UTC, datetime
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
from typing import Any, Literal
|
|
16
15
|
|
|
@@ -74,7 +73,7 @@ def read_json_object(path: Path) -> dict[str, Any]:
|
|
|
74
73
|
except json.JSONDecodeError as exc:
|
|
75
74
|
raise ValueError(f"Malformed JSON in {path}: {exc.msg}") from exc
|
|
76
75
|
if not isinstance(parsed, dict):
|
|
77
|
-
raise
|
|
76
|
+
raise TypeError(f"Expected JSON object in {path}")
|
|
78
77
|
return parsed
|
|
79
78
|
|
|
80
79
|
|
|
@@ -83,7 +82,7 @@ def write_json_with_backup(path: Path, data: dict[str, Any]) -> Path | None:
|
|
|
83
82
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
83
|
backup_path: Path | None = None
|
|
85
84
|
if path.exists():
|
|
86
|
-
timestamp = datetime.now(
|
|
85
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%d%H%M%S%f")
|
|
87
86
|
backup_path = path.with_name(f"{path.name}.bak.{timestamp}")
|
|
88
87
|
index = 1
|
|
89
88
|
while backup_path.exists():
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Validate plugin and marketplace metadata consistency."""
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
@@ -35,7 +34,7 @@ def read_json(path: Path) -> dict[str, Any]:
|
|
|
35
34
|
except FileNotFoundError as exc:
|
|
36
35
|
raise FileNotFoundError(f"Missing required metadata file: {path}") from exc
|
|
37
36
|
if not isinstance(parsed, dict):
|
|
38
|
-
raise
|
|
37
|
+
raise TypeError(f"Expected JSON object in {path}")
|
|
39
38
|
return parsed
|
|
40
39
|
|
|
41
40
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Validate datoon skill copies and packaged .skill artifact consistency."""
|
|
3
2
|
|
|
4
3
|
from __future__ import annotations
|
|
@@ -56,8 +55,7 @@ def validate() -> list[str]:
|
|
|
56
55
|
mirror_hash = _sha256_bytes(mirror_bytes)
|
|
57
56
|
if mirror_hash != source_hash:
|
|
58
57
|
errors.append(
|
|
59
|
-
f"Out-of-sync SKILL.md copy: {mirror_path} "
|
|
60
|
-
f"(expected hash {source_hash}, got {mirror_hash})"
|
|
58
|
+
f"Out-of-sync SKILL.md copy: {mirror_path} (expected hash {source_hash}, got {mirror_hash})"
|
|
61
59
|
)
|
|
62
60
|
|
|
63
61
|
archived_bytes = _read_archive_member(SKILL_ARCHIVE_PATH, SKILL_ARCHIVE_MEMBER)
|
|
@@ -12,17 +12,21 @@ from datoon.models import (
|
|
|
12
12
|
PayloadAnalysis,
|
|
13
13
|
)
|
|
14
14
|
from datoon.readers import detect_format, read_tabular
|
|
15
|
+
from datoon.tokenizers import TokenCounter, estimate_tokens, resolve_tokenizer
|
|
15
16
|
|
|
16
17
|
__all__ = [
|
|
17
|
-
"__version__",
|
|
18
|
-
"convert_json_for_llm",
|
|
19
|
-
"read_tabular",
|
|
20
|
-
"detect_format",
|
|
21
18
|
"ConversionConfig",
|
|
22
19
|
"ConversionOutcome",
|
|
23
20
|
"ConversionReport",
|
|
24
|
-
"PayloadAnalysis",
|
|
25
21
|
"DatoonError",
|
|
22
|
+
"PayloadAnalysis",
|
|
23
|
+
"TokenCounter",
|
|
24
|
+
"__version__",
|
|
25
|
+
"convert_json_for_llm",
|
|
26
|
+
"detect_format",
|
|
27
|
+
"estimate_tokens",
|
|
28
|
+
"read_tabular",
|
|
29
|
+
"resolve_tokenizer",
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
try:
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '3.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (3, 0, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gb00f5623a'
|
|
@@ -4,14 +4,16 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import sys
|
|
8
9
|
from collections.abc import Sequence
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from datoon import __version__
|
|
12
13
|
from datoon.converter import DatoonError, convert_json_for_llm
|
|
13
|
-
from datoon.models import
|
|
14
|
+
from datoon.models import ConversionConfig
|
|
14
15
|
from datoon.readers import ALL_FORMATS, BINARY_FORMATS, detect_format, read_tabular
|
|
16
|
+
from datoon.tokenizers import DEFAULT_TOKENIZER, TOKENIZER_ENV_VAR
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -82,11 +84,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
82
84
|
help="Table index within a Numbers sheet (default: 0).",
|
|
83
85
|
)
|
|
84
86
|
parser.add_argument(
|
|
85
|
-
"--
|
|
86
|
-
default=
|
|
87
|
+
"--tokenizer",
|
|
88
|
+
default=None,
|
|
89
|
+
metavar="SPEC",
|
|
87
90
|
help=(
|
|
88
|
-
"
|
|
89
|
-
|
|
91
|
+
"Token counter for estimates: auto, chars[:N], tiktoken[:ENCODING], "
|
|
92
|
+
"claude:MODEL_ID (network, needs datoon[claude]) or hf:REPO_ID "
|
|
93
|
+
f"(needs datoon[hf]). Default: ${TOKENIZER_ENV_VAR}, "
|
|
94
|
+
f"else {DEFAULT_TOKENIZER}."
|
|
90
95
|
),
|
|
91
96
|
)
|
|
92
97
|
parser.add_argument(
|
|
@@ -172,7 +177,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
172
177
|
max_depth=args.max_depth,
|
|
173
178
|
min_uniform_rows=args.min_uniform_rows,
|
|
174
179
|
force=args.force,
|
|
175
|
-
|
|
180
|
+
tokenizer=(
|
|
181
|
+
args.tokenizer or os.environ.get(TOKENIZER_ENV_VAR) or DEFAULT_TOKENIZER
|
|
182
|
+
),
|
|
176
183
|
)
|
|
177
184
|
|
|
178
185
|
fmt = _resolve_format(args.input, args.format)
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import math
|
|
7
|
-
from functools import lru_cache
|
|
8
6
|
from typing import Any
|
|
9
7
|
|
|
10
8
|
from toon_format import encode
|
|
@@ -12,12 +10,12 @@ from toon_format import encode
|
|
|
12
10
|
from datoon.analyzer import analyze_payload
|
|
13
11
|
from datoon.errors import DatoonError
|
|
14
12
|
from datoon.models import (
|
|
15
|
-
DEFAULT_TOKEN_ENCODING,
|
|
16
13
|
ConversionConfig,
|
|
17
14
|
ConversionOutcome,
|
|
18
15
|
ConversionReport,
|
|
19
16
|
PayloadAnalysis,
|
|
20
17
|
)
|
|
18
|
+
from datoon.tokenizers import estimate_tokens, resolve_tokenizer
|
|
21
19
|
|
|
22
20
|
__all__ = ["DatoonError", "convert_json_for_llm", "estimate_tokens"]
|
|
23
21
|
|
|
@@ -42,26 +40,6 @@ def _normalize_json(raw_text: str) -> tuple[Any, str]:
|
|
|
42
40
|
return parsed, normalized
|
|
43
41
|
|
|
44
42
|
|
|
45
|
-
@lru_cache(maxsize=8)
|
|
46
|
-
def _load_token_encoder(encoding: str) -> Any | None:
|
|
47
|
-
"""Load a token encoder once per name, None when the optional dep is absent."""
|
|
48
|
-
try:
|
|
49
|
-
import tiktoken # type: ignore
|
|
50
|
-
except ImportError:
|
|
51
|
-
return None
|
|
52
|
-
|
|
53
|
-
return tiktoken.get_encoding(encoding)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def estimate_tokens(text: str, encoding: str = DEFAULT_TOKEN_ENCODING) -> int:
|
|
57
|
-
"""Estimate token count with tiktoken when installed, else use a char heuristic."""
|
|
58
|
-
encoder = _load_token_encoder(encoding)
|
|
59
|
-
if encoder is not None:
|
|
60
|
-
return len(encoder.encode(text))
|
|
61
|
-
|
|
62
|
-
return max(1, math.ceil(len(text) / 3))
|
|
63
|
-
|
|
64
|
-
|
|
65
43
|
def _encode_toon(parsed: Any) -> str:
|
|
66
44
|
"""Encode an already-parsed JSON value as TOON with the official encoder."""
|
|
67
45
|
try:
|
|
@@ -81,6 +59,7 @@ def _build_skip_outcome(
|
|
|
81
59
|
reason: str,
|
|
82
60
|
config: ConversionConfig,
|
|
83
61
|
input_tokens: int,
|
|
62
|
+
tokenizer_name: str,
|
|
84
63
|
analysis: PayloadAnalysis,
|
|
85
64
|
) -> ConversionOutcome:
|
|
86
65
|
"""Build a standard skip outcome to keep reporting consistent."""
|
|
@@ -91,16 +70,23 @@ def _build_skip_outcome(
|
|
|
91
70
|
input_token_estimate=input_tokens,
|
|
92
71
|
output_token_estimate=input_tokens,
|
|
93
72
|
savings_ratio=0.0,
|
|
73
|
+
tokenizer=tokenizer_name,
|
|
94
74
|
analysis=analysis,
|
|
95
75
|
)
|
|
96
76
|
return ConversionOutcome(payload_text=normalized_json, report=report)
|
|
97
77
|
|
|
98
78
|
|
|
99
79
|
def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionOutcome:
|
|
100
|
-
"""Convert JSON to TOON when structure and savings meet configured policy.
|
|
80
|
+
"""Convert JSON to TOON when structure and savings meet configured policy.
|
|
81
|
+
|
|
82
|
+
The tokenizer is resolved once per call and counts both the JSON and the
|
|
83
|
+
TOON text. A tokenizer that cannot be resolved is a configuration error and
|
|
84
|
+
raises ``DatoonError`` in auto and force mode alike.
|
|
85
|
+
"""
|
|
101
86
|
parsed, normalized_json = _normalize_json(raw_text)
|
|
87
|
+
counter, tokenizer_name = resolve_tokenizer(config.tokenizer)
|
|
102
88
|
analysis = analyze_payload(parsed, config)
|
|
103
|
-
input_tokens = estimate_tokens(normalized_json,
|
|
89
|
+
input_tokens = estimate_tokens(normalized_json, counter)
|
|
104
90
|
|
|
105
91
|
if not analysis.is_candidate and not config.force:
|
|
106
92
|
return _build_skip_outcome(
|
|
@@ -108,6 +94,7 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
108
94
|
reason=analysis.reason,
|
|
109
95
|
config=config,
|
|
110
96
|
input_tokens=input_tokens,
|
|
97
|
+
tokenizer_name=tokenizer_name,
|
|
111
98
|
analysis=analysis,
|
|
112
99
|
)
|
|
113
100
|
|
|
@@ -122,10 +109,11 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
122
109
|
reason="TOON encoding failed; falling back to JSON.",
|
|
123
110
|
config=config,
|
|
124
111
|
input_tokens=input_tokens,
|
|
112
|
+
tokenizer_name=tokenizer_name,
|
|
125
113
|
analysis=analysis,
|
|
126
114
|
)
|
|
127
115
|
|
|
128
|
-
output_tokens = estimate_tokens(toon_text,
|
|
116
|
+
output_tokens = estimate_tokens(toon_text, counter)
|
|
129
117
|
savings_ratio = (input_tokens - output_tokens) / input_tokens
|
|
130
118
|
|
|
131
119
|
if savings_ratio < config.min_savings_ratio and not config.force:
|
|
@@ -137,6 +125,7 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
137
125
|
),
|
|
138
126
|
config=config,
|
|
139
127
|
input_tokens=input_tokens,
|
|
128
|
+
tokenizer_name=tokenizer_name,
|
|
140
129
|
analysis=analysis,
|
|
141
130
|
)
|
|
142
131
|
|
|
@@ -150,6 +139,7 @@ def convert_json_for_llm(raw_text: str, config: ConversionConfig) -> ConversionO
|
|
|
150
139
|
input_token_estimate=input_tokens,
|
|
151
140
|
output_token_estimate=output_tokens,
|
|
152
141
|
savings_ratio=savings_ratio,
|
|
142
|
+
tokenizer=tokenizer_name,
|
|
153
143
|
analysis=analysis,
|
|
154
144
|
)
|
|
155
145
|
return ConversionOutcome(payload_text=toon_text, report=report)
|