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.
Files changed (135) hide show
  1. {datoon-2.0.0 → datoon-3.0.0}/.gitignore +1 -0
  2. {datoon-2.0.0 → datoon-3.0.0}/CHANGELOG.md +19 -0
  3. {datoon-2.0.0 → datoon-3.0.0}/CLAUDE.md +4 -1
  4. {datoon-2.0.0 → datoon-3.0.0}/INSTALL.md +20 -0
  5. {datoon-2.0.0 → datoon-3.0.0}/PKG-INFO +27 -3
  6. {datoon-2.0.0 → datoon-3.0.0}/README.md +21 -1
  7. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/generate_payloads.py +3 -6
  8. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/run.py +17 -5
  9. {datoon-2.0.0 → datoon-3.0.0}/pyproject.toml +3 -1
  10. {datoon-2.0.0 → datoon-3.0.0}/scripts/ab_skill_benchmark.py +3 -4
  11. {datoon-2.0.0 → datoon-3.0.0}/scripts/install.py +3 -4
  12. {datoon-2.0.0 → datoon-3.0.0}/scripts/summarize_agent_skill_eval.py +0 -1
  13. {datoon-2.0.0 → datoon-3.0.0}/scripts/validate_plugin_metadata.py +1 -2
  14. {datoon-2.0.0 → datoon-3.0.0}/scripts/validate_skill_sync.py +1 -3
  15. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/__init__.py +9 -5
  16. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/_version.py +3 -3
  17. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/cli.py +13 -6
  18. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/converter.py +16 -26
  19. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/mcp_server.py +39 -5
  20. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/models.py +9 -7
  21. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/columnar.py +1 -1
  22. datoon-3.0.0/src/datoon/tokenizers.py +298 -0
  23. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/PKG-INFO +27 -3
  24. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/SOURCES.txt +3 -0
  25. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/requires.txt +7 -1
  26. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/scm_file_list.json +3 -0
  27. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/scm_version.json +2 -2
  28. datoon-3.0.0/tests/conftest.py +11 -0
  29. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_cli.py +35 -10
  30. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_converter_policy.py +42 -2
  31. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_errors.py +1 -1
  32. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_mcp_server.py +42 -1
  33. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_models.py +15 -0
  34. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_readers.py +2 -10
  35. datoon-3.0.0/tests/unit/test_tokenizers.py +366 -0
  36. datoon-3.0.0/tests/unit/test_tokens.py +75 -0
  37. {datoon-2.0.0 → datoon-3.0.0}/uv.lock +249 -8
  38. datoon-2.0.0/tests/unit/test_tokens.py +0 -43
  39. {datoon-2.0.0 → datoon-3.0.0}/.agents/plugins/marketplace.json +0 -0
  40. {datoon-2.0.0 → datoon-3.0.0}/.claude-plugin/marketplace.json +0 -0
  41. {datoon-2.0.0 → datoon-3.0.0}/.claude-plugin/plugin.json +0 -0
  42. {datoon-2.0.0 → datoon-3.0.0}/.editorconfig +0 -0
  43. {datoon-2.0.0 → datoon-3.0.0}/.gitattributes +0 -0
  44. {datoon-2.0.0 → datoon-3.0.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  45. {datoon-2.0.0 → datoon-3.0.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  46. {datoon-2.0.0 → datoon-3.0.0}/.github/dependabot.yml +0 -0
  47. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/package-installer-smoke.yml +0 -0
  48. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/pages.yml +0 -0
  49. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/pre-commit.yml +0 -0
  50. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/publish-mcp.yml +0 -0
  51. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/publish.yml +0 -0
  52. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/release.yml +0 -0
  53. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/tests.yml +0 -0
  54. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/validate-artifacts.yml +0 -0
  55. {datoon-2.0.0 → datoon-3.0.0}/.github/workflows/workflow-lint.yml +0 -0
  56. {datoon-2.0.0 → datoon-3.0.0}/.pre-commit-config.yaml +0 -0
  57. {datoon-2.0.0 → datoon-3.0.0}/.releaserc.yaml +0 -0
  58. {datoon-2.0.0 → datoon-3.0.0}/CONTRIBUTING.md +0 -0
  59. {datoon-2.0.0 → datoon-3.0.0}/LICENSE +0 -0
  60. {datoon-2.0.0 → datoon-3.0.0}/MARKETPLACES.md +0 -0
  61. {datoon-2.0.0 → datoon-3.0.0}/SECURITY.md +0 -0
  62. {datoon-2.0.0 → datoon-3.0.0}/SKILL.md +0 -0
  63. {datoon-2.0.0 → datoon-3.0.0}/assets/datoon-banner.svg +0 -0
  64. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/REPORT.md +0 -0
  65. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/agent_results.json +0 -0
  66. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/expected_answers.json +0 -0
  67. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_large.json +0 -0
  68. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_medium.json +0 -0
  69. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/1_small.json +0 -0
  70. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_large.json +0 -0
  71. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_medium.json +0 -0
  72. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/2_small.json +0 -0
  73. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_large.json +0 -0
  74. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_medium.json +0 -0
  75. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/payloads/3_small.json +0 -0
  76. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_large.report.json +0 -0
  77. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_medium.report.json +0 -0
  78. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/1_small.report.json +0 -0
  79. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_large.report.json +0 -0
  80. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_medium.report.json +0 -0
  81. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/2_small.report.json +0 -0
  82. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_large.report.json +0 -0
  83. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_medium.report.json +0 -0
  84. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/reports/3_small.report.json +0 -0
  85. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/scored_agent_results.json +0 -0
  86. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_large.toon +0 -0
  87. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_medium.toon +0 -0
  88. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/1_small.toon +0 -0
  89. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_large.toon +0 -0
  90. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_medium.toon +0 -0
  91. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/2_small.toon +0 -0
  92. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_large.toon +0 -0
  93. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_medium.toon +0 -0
  94. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/agent_skill_eval/toon/3_small.toon +0 -0
  95. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/payloads.json +0 -0
  96. {datoon-2.0.0 → datoon-3.0.0}/benchmarks/results/.gitkeep +0 -0
  97. {datoon-2.0.0 → datoon-3.0.0}/datoon/SKILL.md +0 -0
  98. {datoon-2.0.0 → datoon-3.0.0}/datoon.skill +0 -0
  99. {datoon-2.0.0 → datoon-3.0.0}/docs/.nojekyll +0 -0
  100. {datoon-2.0.0 → datoon-3.0.0}/docs/app.js +0 -0
  101. {datoon-2.0.0 → datoon-3.0.0}/docs/assets/datoon-banner.svg +0 -0
  102. {datoon-2.0.0 → datoon-3.0.0}/docs/index.html +0 -0
  103. {datoon-2.0.0 → datoon-3.0.0}/docs/styles.css +0 -0
  104. {datoon-2.0.0 → datoon-3.0.0}/glama.json +0 -0
  105. {datoon-2.0.0 → datoon-3.0.0}/install.sh +0 -0
  106. {datoon-2.0.0 → datoon-3.0.0}/plugins/datoon/.codex-plugin/plugin.json +0 -0
  107. {datoon-2.0.0 → datoon-3.0.0}/plugins/datoon/skills/datoon/SKILL.md +0 -0
  108. {datoon-2.0.0 → datoon-3.0.0}/server.json +0 -0
  109. {datoon-2.0.0 → datoon-3.0.0}/setup.cfg +0 -0
  110. {datoon-2.0.0 → datoon-3.0.0}/setup.sh +0 -0
  111. {datoon-2.0.0 → datoon-3.0.0}/skills/datoon/README.md +0 -0
  112. {datoon-2.0.0 → datoon-3.0.0}/skills/datoon/SKILL.md +0 -0
  113. {datoon-2.0.0 → datoon-3.0.0}/smithery.yaml +0 -0
  114. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/analyzer.py +0 -0
  115. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/errors.py +0 -0
  116. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/py.typed +0 -0
  117. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/__init__.py +0 -0
  118. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/_coerce.py +0 -0
  119. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/_tabular.py +0 -0
  120. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/csv.py +0 -0
  121. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/excel.py +0 -0
  122. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/jsonl.py +0 -0
  123. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/numbers.py +0 -0
  124. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/xml.py +0 -0
  125. {datoon-2.0.0 → datoon-3.0.0}/src/datoon/readers/yaml.py +0 -0
  126. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/dependency_links.txt +0 -0
  127. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/entry_points.txt +0 -0
  128. {datoon-2.0.0 → datoon-3.0.0}/src/datoon.egg-info/top_level.txt +0 -0
  129. {datoon-2.0.0 → datoon-3.0.0}/tests/fixtures/users.expected.toon +0 -0
  130. {datoon-2.0.0 → datoon-3.0.0}/tests/fixtures/users.json +0 -0
  131. {datoon-2.0.0 → datoon-3.0.0}/tests/integration/test_toon_roundtrip.py +0 -0
  132. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_analyzer.py +0 -0
  133. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_benchmark_readme.py +0 -0
  134. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_installer.py +0 -0
  135. {datoon-2.0.0 → datoon-3.0.0}/tests/unit/test_tabular_rows.py +0 -0
@@ -40,6 +40,7 @@ benchmarks/results/**/*.json
40
40
  # Local Codex/Claude artifacts
41
41
  .agents/skills/
42
42
  .claude/
43
+ .omc/
43
44
  skills-lock.json
44
45
 
45
46
  # Generated by setuptools-scm during local builds/tests
@@ -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: 2.0.0
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 token estimates. Pipelines never get silent surprises.
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 token estimates. Pipelines never get silent surprises.
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 = sorted(region_totals.items(), key=lambda item: (-item[1], item[0]))[0][
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 datetime, timezone
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(timezone.utc).strftime("%Y%m%d_%H%M%S")
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(timezone.utc).isoformat(),
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 datetime, timezone
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(timezone.utc).strftime("%Y%m%d_%H%M%S")
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(timezone.utc).isoformat(),
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 datetime, timezone
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 ValueError(f"Expected JSON object in {path}")
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(timezone.utc).strftime("%Y%m%d%H%M%S%f")
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
  """Summarize artifact-based agent skill evaluation results."""
3
2
 
4
3
  from __future__ import annotations
@@ -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 ValueError(f"Expected JSON object in {path}")
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 = '2.0.0'
22
- __version_tuple__ = version_tuple = (2, 0, 0)
21
+ __version__ = version = '3.0.0'
22
+ __version_tuple__ = version_tuple = (3, 0, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g02282b3f8'
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 DEFAULT_TOKEN_ENCODING, ConversionConfig
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
- "--encoding",
86
- default=DEFAULT_TOKEN_ENCODING,
87
+ "--tokenizer",
88
+ default=None,
89
+ metavar="SPEC",
87
90
  help=(
88
- "tiktoken encoding for token estimates "
89
- f"(default: {DEFAULT_TOKEN_ENCODING})."
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
- token_encoding=args.encoding,
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, config.token_encoding)
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, config.token_encoding)
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)