token0 0.3.2__tar.gz → 0.4.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 (94) hide show
  1. token0-0.4.0/.github/workflows/publish.yml +26 -0
  2. {token0-0.3.2 → token0-0.4.0}/PKG-INFO +114 -15
  3. {token0-0.3.2 → token0-0.4.0}/README.md +112 -13
  4. token0-0.4.0/benchmarks/bench_ax_tree.py +359 -0
  5. token0-0.4.0/benchmarks/bench_ax_tree_models.py +422 -0
  6. token0-0.4.0/benchmarks/bench_ax_tree_real.py +686 -0
  7. {token0-0.3.2 → token0-0.4.0}/pyproject.toml +1 -1
  8. token0-0.4.0/tests/test_ax_tree.py +229 -0
  9. token0-0.4.0/tests/test_config.py +42 -0
  10. {token0-0.3.2 → token0-0.4.0}/tests/test_litellm_hook.py +70 -2
  11. token0-0.4.0/tests/test_ocr_guard.py +79 -0
  12. {token0-0.3.2 → token0-0.4.0}/tests/test_router.py +40 -6
  13. {token0-0.3.2 → token0-0.4.0}/token0/api/v1/chat.py +85 -22
  14. {token0-0.3.2 → token0-0.4.0}/token0/api/v1/estimate.py +15 -5
  15. {token0-0.3.2 → token0-0.4.0}/token0/config.py +18 -1
  16. token0-0.4.0/token0/litellm_hook.py +151 -0
  17. {token0-0.3.2 → token0-0.4.0}/token0/models/request.py +7 -1
  18. token0-0.4.0/token0/optimization/ax_tree.py +157 -0
  19. {token0-0.3.2 → token0-0.4.0}/token0/optimization/message_optimizer.py +16 -11
  20. {token0-0.3.2 → token0-0.4.0}/token0/optimization/prompt_classifier.py +45 -0
  21. {token0-0.3.2 → token0-0.4.0}/token0/optimization/router.py +65 -8
  22. {token0-0.3.2 → token0-0.4.0}/token0/optimization/transformer.py +35 -0
  23. {token0-0.3.2 → token0-0.4.0}/token0_integration.md +13 -13
  24. token0-0.3.2/.claude/settings.local.json +0 -48
  25. token0-0.3.2/token0/litellm_hook.py +0 -85
  26. {token0-0.3.2 → token0-0.4.0}/.env.example +0 -0
  27. {token0-0.3.2 → token0-0.4.0}/.github/workflows/ci.yml +0 -0
  28. {token0-0.3.2 → token0-0.4.0}/.gitignore +0 -0
  29. {token0-0.3.2 → token0-0.4.0}/Dockerfile +0 -0
  30. {token0-0.3.2 → token0-0.4.0}/LICENSE +0 -0
  31. {token0-0.3.2 → token0-0.4.0}/alembic/env.py +0 -0
  32. {token0-0.3.2 → token0-0.4.0}/alembic/script.py.mako +0 -0
  33. {token0-0.3.2 → token0-0.4.0}/alembic.ini +0 -0
  34. {token0-0.3.2 → token0-0.4.0}/benchmarks/__init__.py +0 -0
  35. {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_fuzzy_cache.py +0 -0
  36. {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_video.py +0 -0
  37. {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_video_models.py +0 -0
  38. {token0-0.3.2 → token0-0.4.0}/benchmarks/generate_test_images.py +0 -0
  39. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/already_optimized.jpg +0 -0
  40. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/document_screenshot.png +0 -0
  41. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/large_photo.jpg +0 -0
  42. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/large_screenshot.png +0 -0
  43. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/document_invoice.png +0 -0
  44. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/photo_nature.jpg +0 -0
  45. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/photo_street.jpg +0 -0
  46. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/receipt_real.jpg +0 -0
  47. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/receipt.png +0 -0
  48. {token0-0.3.2 → token0-0.4.0}/benchmarks/images/small_photo.jpg +0 -0
  49. {token0-0.3.2 → token0-0.4.0}/benchmarks/run.py +0 -0
  50. {token0-0.3.2 → token0-0.4.0}/docker-compose.yml +0 -0
  51. {token0-0.3.2 → token0-0.4.0}/test_token0_litellm.py +0 -0
  52. {token0-0.3.2 → token0-0.4.0}/tests/__init__.py +0 -0
  53. {token0-0.3.2 → token0-0.4.0}/tests/conftest.py +0 -0
  54. {token0-0.3.2 → token0-0.4.0}/tests/test_analyzer.py +0 -0
  55. {token0-0.3.2 → token0-0.4.0}/tests/test_api.py +0 -0
  56. {token0-0.3.2 → token0-0.4.0}/tests/test_cache.py +0 -0
  57. {token0-0.3.2 → token0-0.4.0}/tests/test_estimate.py +0 -0
  58. {token0-0.3.2 → token0-0.4.0}/tests/test_instructor_hook.py +0 -0
  59. {token0-0.3.2 → token0-0.4.0}/tests/test_langchain_callback.py +0 -0
  60. {token0-0.3.2 → token0-0.4.0}/tests/test_pdf.py +0 -0
  61. {token0-0.3.2 → token0-0.4.0}/tests/test_providers.py +0 -0
  62. {token0-0.3.2 → token0-0.4.0}/tests/test_saliency.py +0 -0
  63. {token0-0.3.2 → token0-0.4.0}/tests/test_streaming.py +0 -0
  64. {token0-0.3.2 → token0-0.4.0}/tests/test_transformer.py +0 -0
  65. {token0-0.3.2 → token0-0.4.0}/tests/test_video.py +0 -0
  66. {token0-0.3.2 → token0-0.4.0}/token0/__init__.py +0 -0
  67. {token0-0.3.2 → token0-0.4.0}/token0/api/__init__.py +0 -0
  68. {token0-0.3.2 → token0-0.4.0}/token0/api/auth.py +0 -0
  69. {token0-0.3.2 → token0-0.4.0}/token0/api/v1/__init__.py +0 -0
  70. {token0-0.3.2 → token0-0.4.0}/token0/api/v1/usage.py +0 -0
  71. {token0-0.3.2 → token0-0.4.0}/token0/cli.py +0 -0
  72. {token0-0.3.2 → token0-0.4.0}/token0/instructor_hook.py +0 -0
  73. {token0-0.3.2 → token0-0.4.0}/token0/langchain_callback.py +0 -0
  74. {token0-0.3.2 → token0-0.4.0}/token0/main.py +0 -0
  75. {token0-0.3.2 → token0-0.4.0}/token0/models/__init__.py +0 -0
  76. {token0-0.3.2 → token0-0.4.0}/token0/models/db.py +0 -0
  77. {token0-0.3.2 → token0-0.4.0}/token0/optimization/__init__.py +0 -0
  78. {token0-0.3.2 → token0-0.4.0}/token0/optimization/analyzer.py +0 -0
  79. {token0-0.3.2 → token0-0.4.0}/token0/optimization/cache.py +0 -0
  80. {token0-0.3.2 → token0-0.4.0}/token0/optimization/pdf.py +0 -0
  81. {token0-0.3.2 → token0-0.4.0}/token0/optimization/saliency.py +0 -0
  82. {token0-0.3.2 → token0-0.4.0}/token0/optimization/video.py +0 -0
  83. {token0-0.3.2 → token0-0.4.0}/token0/providers/__init__.py +0 -0
  84. {token0-0.3.2 → token0-0.4.0}/token0/providers/anthropic.py +0 -0
  85. {token0-0.3.2 → token0-0.4.0}/token0/providers/base.py +0 -0
  86. {token0-0.3.2 → token0-0.4.0}/token0/providers/google.py +0 -0
  87. {token0-0.3.2 → token0-0.4.0}/token0/providers/ollama.py +0 -0
  88. {token0-0.3.2 → token0-0.4.0}/token0/providers/openai.py +0 -0
  89. {token0-0.3.2 → token0-0.4.0}/token0/static/dashboard.html +0 -0
  90. {token0-0.3.2 → token0-0.4.0}/token0/storage/__init__.py +0 -0
  91. {token0-0.3.2 → token0-0.4.0}/token0/storage/objects.py +0 -0
  92. {token0-0.3.2 → token0-0.4.0}/token0/storage/postgres.py +0 -0
  93. {token0-0.3.2 → token0-0.4.0}/token0/storage/redis.py +0 -0
  94. {token0-0.3.2 → token0-0.4.0}/uv.lock +0 -0
@@ -0,0 +1,26 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ pypi:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - name: Set up Python
14
+ uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.12"
17
+
18
+ - name: Build package
19
+ run: |
20
+ python -m pip install --upgrade pip build
21
+ python -m build
22
+
23
+ - name: Publish to PyPI
24
+ uses: pypa/gh-action-pypi-publish@release/v1
25
+ with:
26
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: token0
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Open-source API proxy that makes vision LLM calls 5-10x cheaper
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -60,6 +60,21 @@ Token0 fixes this. It sits between your app and the LLM, analyzes every image an
60
60
 
61
61
  ---
62
62
 
63
+ ## When Token0 Helps (Measured)
64
+
65
+ Real requests through the LiteLLM Proxy integration, GPT-4.1 token formulas:
66
+
67
+ | Request | What the provider receives | Image tokens |
68
+ |---|---|---|
69
+ | "Classify this image", 4000×3000 photo | 512×384, low detail (40 KB instead of 1.4 MB) | 765 → 85 |
70
+ | "Extract all text", 200dpi document page | OCR text (verified cheaper than the image) | 1,105 → 815 |
71
+ | "Read the text on this page", dense 1080p screenshot | image — OCR text would cost 1,513 | 1,105 → 1,105 |
72
+ | "Click the add to cart button", 1080p screenshot | original dimensions kept, PNG → JPEG | 1,105 → 1,105 |
73
+
74
+ - **Biggest wins:** simple tasks on large photos (classify, yes/no, count) and document pages.
75
+ - **No token savings on agent screenshots, by design:** UI/computer-use prompts keep the original dimensions so the coordinates the model returns still map onto the screen; only the upload gets smaller.
76
+ - **OCR is verified, not assumed:** Token0 extracts the text, and if it would cost more than the (optimized) image, it sends the image instead. OCR runs on CPU via EasyOCR and adds seconds per image, so it suits batch/document pipelines more than latency-sensitive calls.
77
+
63
78
  ## How It Works
64
79
 
65
80
  ```
@@ -68,7 +83,7 @@ Your App → Token0 Proxy → [Analyze → Classify → Route → Transform →
68
83
  Database (logs every optimization decision + savings)
69
84
  ```
70
85
 
71
- Token0 applies **11 optimizations** automatically:
86
+ Token0 applies **12 optimizations** automatically:
72
87
 
73
88
  ### Core Optimizations (Free Tier)
74
89
 
@@ -96,6 +111,8 @@ Token0 applies **11 optimizations** automatically:
96
111
 
97
112
  **11. Saliency-Based ROI Cropping** — Detects which region of an image the prompt is asking about and crops to that region before sending to the LLM. "What's the total on this invoice?" → crops to the bottom 40% of the image. "Read the header" → crops to the top 25%. Rule-based spatial keyword matching (zero ML deps). Delivers ~60% additional pixel reduction on document and form images before any other optimization runs.
98
113
 
114
+ **12. Accessibility Tree Routing** — UI automation agents often have both a screenshot and an accessibility tree (AXUIElement, Playwright, Chrome DevTools). Token0 accepts both and routes to the cheaper representation automatically. If the tree is complete (no canvas/iframe/opaque elements), the screenshot is dropped and the tree is serialized as compact text — **93-97% token savings** vs a 1080p screenshot. If the tree has opaque nodes (a Figma canvas, a video element), Token0 keeps the screenshot. Supports Playwright/CDP format, macOS AXUIElement format, and pre-serialized strings.
115
+
99
116
  ---
100
117
 
101
118
  ## Benchmarks
@@ -240,10 +257,58 @@ Using OpenAI's published token formulas on real images and GPT-4.1 pricing ($2.0
240
257
  9. **On cloud APIs, total image savings reach 98.9%** when all optimizations are combined with model cascading.
241
258
  10. **Video deduplication collapses 60-frame clips to ~10 keyframes** — 13-45% savings on local models, ~83% projected on GPT-4.1.
242
259
  11. **Model-aware OCR skip is critical** — ultra-efficient encoders like llama3.2-vision use <50 tokens/image; OCR text output would cost more, not less.
260
+ 12. **Accessibility tree routing eliminates screenshot cost entirely** for UI agents — 93-97% savings when the tree is complete; screenshot fallback is automatic when canvas/iframe nodes are detected.
261
+
262
+ ### Accessibility Tree Benchmark (GPT-4o pricing)
263
+
264
+ UI agents that send both a screenshot and an accessibility tree can route to the cheaper representation automatically.
265
+
266
+ **Real browser results — Playwright, 1280×720, live pages** (actual reported prompt_tokens):
267
+
268
+ | Page | Screenshot Tokens | Tree Tokens | Savings | Model |
269
+ |---|---|---|---|---|
270
+ | Hacker News | 750 | 192 | **74.4%** | moondream |
271
+ | Hacker News | 602 | 164 | **72.8%** | llava:7b |
272
+ | GitHub Home | 751 | 560 | **25.4%** | moondream |
273
+ | GitHub Home | 601 | 560 | **6.8%** | llava:7b |
274
+ | Wikipedia | 747 | 747 | **0%** | moondream |
275
+ | Wikipedia | 599 | 1,165 | **-94.5%** | llava:7b — tree too large |
276
+
277
+ > Wikipedia's rich navigation tree exceeded the screenshot token count on llava:7b — token0 would correctly fall back to the screenshot in this case. Hacker News (minimal DOM) shows the best real-world savings.
278
+
279
+ **Ollama model results — 7 vision models, synthetic 800×600 screenshots** (actual reported prompt_tokens):
280
+
281
+ > Synthetic screenshots: PIL-generated images with drawn UI elements (login form, todo list). Not real browser screenshots.
282
+
283
+ | Model | Screenshot Tokens | Tree Tokens | Savings | Note |
284
+ |---|---|---|---|---|
285
+ | granite3.2-vision | 10,328 | 218 | **97.9%** | High-res encoder |
286
+ | moondream | 1,500 | 168 | **88.8%** | |
287
+ | llava:7b | 1,202 | 160 | **86.7%** | |
288
+ | llava-llama3 | 1,201 | 164 | **86.3%** | |
289
+ | minicpm-v | 704 | 128 | **81.8%** | |
290
+ | gemma3:4b | 566 | 145 | **74.4%** | |
291
+ | llama3.2-vision | 46 | 130 | n/a | Ultra-efficient encoder — tree costs more; screenshot wins |
292
+
293
+ **Cloud API extrapolation** (tree tokens from Ollama measurements, screenshot tokens from published formulas, 800×600 image):
294
+
295
+ | Provider | Screenshot Tokens | Tree Tokens (avg) | Savings | At 100K calls/day, saved/mo |
296
+ |---|---|---|---|---|
297
+ | OpenAI GPT-4o | 1,530 | ~80 | **89.6%** | **~$10,282** |
298
+ | Anthropic Claude | 1,280 | ~80 | **87.6%** | **~$10,089** |
299
+
300
+ > Tree token counts are text-based and provider-agnostic (~4 chars/token). Screenshot tokens use OpenAI tile formula (85 + 170×tiles) and Anthropic pixel formula (w×h/750). Canvas/iframe nodes trigger automatic screenshot fallback — no configuration needed.
301
+
302
+ Run benchmarks:
303
+ ```bash
304
+ python -m benchmarks.bench_ax_tree # formula-based projections
305
+ python -m benchmarks.bench_ax_tree_models # all 7 Ollama vision models (synthetic)
306
+ python -m benchmarks.bench_ax_tree_real # real browser pages via Playwright
307
+ ```
243
308
 
244
309
  ### Additional Test Coverage
245
310
 
246
- Token0 includes **171 unit tests** and benchmarks across multiple suites:
311
+ Token0 includes **216 unit tests** and benchmarks across multiple suites:
247
312
 
248
313
  | Suite | Tests | What It Validates |
249
314
  |---|---|---|
@@ -260,6 +325,7 @@ Token0 includes **171 unit tests** and benchmarks across multiple suites:
260
325
  | `pdf` | 8 | PDF detection, decode, text extraction, token estimation |
261
326
  | `estimate` | 11 | /v1/estimate endpoint: single image, multiple images, remote URL skip, cost calc |
262
327
  | `langchain` | 8 | LangChain callback: import, text passthrough, image optimization, role mapping |
328
+ | `ax_tree` | 22 | AX tree serialize, opaque detection, AXUIElement format, combo routing |
263
329
 
264
330
  ---
265
331
 
@@ -407,6 +473,40 @@ response = client.chat.completions.create(
407
473
  # ~83% savings on GPT-4.1
408
474
  ```
409
475
 
476
+ ### Accessibility Tree Support (UI Agents)
477
+
478
+ If your agent captures both a screenshot and an accessibility tree, send both — Token0 picks the cheaper path automatically:
479
+
480
+ ```python
481
+ import json
482
+
483
+ # Playwright example
484
+ page = await browser.new_page()
485
+ snapshot = await page.accessibility.snapshot() # returns a dict
486
+
487
+ response = client.chat.completions.create(
488
+ model="gpt-4.1",
489
+ messages=[{
490
+ "role": "user",
491
+ "content": [
492
+ {"type": "text", "text": "What button should I click to submit the form?"},
493
+ # Screenshot fallback — only used if tree has canvas/iframe nodes
494
+ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}},
495
+ # Accessibility tree — token0 routes to this when complete
496
+ {"type": "accessibility_tree", "accessibility_tree": {
497
+ "data": snapshot,
498
+ "source": "playwright"
499
+ }},
500
+ ]
501
+ }],
502
+ extra_headers={"X-Provider-Key": "sk-..."}
503
+ )
504
+ # GitHub PR page: 2,125 tokens (screenshot) → 132 tokens (tree) — 93.8% savings
505
+ # response.token0.optimizations_applied = ["ax tree → text (1,993 tokens saved vs screenshot)"]
506
+ ```
507
+
508
+ Works with **Playwright**, **macOS AXUIElement**, **Chrome DevTools Protocol**, and pre-serialized strings. Canvas, iframe, and video elements trigger automatic screenshot fallback — no configuration needed.
509
+
410
510
  ### Streaming Support
411
511
 
412
512
  Token0 supports `stream=true` — images are optimized before streaming begins, then tokens flow word-by-word via SSE:
@@ -433,16 +533,14 @@ for chunk in stream:
433
533
 
434
534
  ### Use With LiteLLM
435
535
 
436
- Already using [LiteLLM](https://github.com/BerriAI/litellm)? Add Token0 as a callback hook — no proxy needed:
536
+ Already using [LiteLLM](https://github.com/BerriAI/litellm)?
437
537
 
438
- ```python
439
- import litellm
440
- from token0.litellm_hook import Token0Hook
538
+ **SDK** — swap the import; same arguments as `litellm.completion` / `litellm.acompletion`:
441
539
 
442
- litellm.callbacks = [Token0Hook()]
540
+ ```python
541
+ from token0.litellm_hook import completion # or acompletion
443
542
 
444
- # All your existing litellm calls now get image optimization for free
445
- response = litellm.completion(
543
+ response = completion(
446
544
  model="gpt-4.1",
447
545
  messages=[{
448
546
  "role": "user",
@@ -453,15 +551,16 @@ response = litellm.completion(
453
551
  }]
454
552
  )
455
553
 
456
- # Stats available in response metadata
457
- # response._hidden_params["metadata"]["token0"]["tokens_saved"]
554
+ # Stats: response._hidden_params["token0"]["tokens_saved"]
458
555
  ```
459
556
 
460
- Or in LiteLLM proxy `config.yaml`:
557
+ (Registering `Token0Hook()` in `litellm.callbacks` does **not** work with the SDK — LiteLLM only runs pre-call hooks inside the proxy.)
558
+
559
+ **Proxy** — in `config.yaml`:
461
560
 
462
561
  ```yaml
463
562
  litellm_settings:
464
- callbacks: ["token0.litellm_hook.Token0Hook"]
563
+ callbacks: ["token0.litellm_hook.proxy_handler_instance"]
465
564
  ```
466
565
 
467
566
  ### Use With LangChain
@@ -22,6 +22,21 @@ Token0 fixes this. It sits between your app and the LLM, analyzes every image an
22
22
 
23
23
  ---
24
24
 
25
+ ## When Token0 Helps (Measured)
26
+
27
+ Real requests through the LiteLLM Proxy integration, GPT-4.1 token formulas:
28
+
29
+ | Request | What the provider receives | Image tokens |
30
+ |---|---|---|
31
+ | "Classify this image", 4000×3000 photo | 512×384, low detail (40 KB instead of 1.4 MB) | 765 → 85 |
32
+ | "Extract all text", 200dpi document page | OCR text (verified cheaper than the image) | 1,105 → 815 |
33
+ | "Read the text on this page", dense 1080p screenshot | image — OCR text would cost 1,513 | 1,105 → 1,105 |
34
+ | "Click the add to cart button", 1080p screenshot | original dimensions kept, PNG → JPEG | 1,105 → 1,105 |
35
+
36
+ - **Biggest wins:** simple tasks on large photos (classify, yes/no, count) and document pages.
37
+ - **No token savings on agent screenshots, by design:** UI/computer-use prompts keep the original dimensions so the coordinates the model returns still map onto the screen; only the upload gets smaller.
38
+ - **OCR is verified, not assumed:** Token0 extracts the text, and if it would cost more than the (optimized) image, it sends the image instead. OCR runs on CPU via EasyOCR and adds seconds per image, so it suits batch/document pipelines more than latency-sensitive calls.
39
+
25
40
  ## How It Works
26
41
 
27
42
  ```
@@ -30,7 +45,7 @@ Your App → Token0 Proxy → [Analyze → Classify → Route → Transform →
30
45
  Database (logs every optimization decision + savings)
31
46
  ```
32
47
 
33
- Token0 applies **11 optimizations** automatically:
48
+ Token0 applies **12 optimizations** automatically:
34
49
 
35
50
  ### Core Optimizations (Free Tier)
36
51
 
@@ -58,6 +73,8 @@ Token0 applies **11 optimizations** automatically:
58
73
 
59
74
  **11. Saliency-Based ROI Cropping** — Detects which region of an image the prompt is asking about and crops to that region before sending to the LLM. "What's the total on this invoice?" → crops to the bottom 40% of the image. "Read the header" → crops to the top 25%. Rule-based spatial keyword matching (zero ML deps). Delivers ~60% additional pixel reduction on document and form images before any other optimization runs.
60
75
 
76
+ **12. Accessibility Tree Routing** — UI automation agents often have both a screenshot and an accessibility tree (AXUIElement, Playwright, Chrome DevTools). Token0 accepts both and routes to the cheaper representation automatically. If the tree is complete (no canvas/iframe/opaque elements), the screenshot is dropped and the tree is serialized as compact text — **93-97% token savings** vs a 1080p screenshot. If the tree has opaque nodes (a Figma canvas, a video element), Token0 keeps the screenshot. Supports Playwright/CDP format, macOS AXUIElement format, and pre-serialized strings.
77
+
61
78
  ---
62
79
 
63
80
  ## Benchmarks
@@ -202,10 +219,58 @@ Using OpenAI's published token formulas on real images and GPT-4.1 pricing ($2.0
202
219
  9. **On cloud APIs, total image savings reach 98.9%** when all optimizations are combined with model cascading.
203
220
  10. **Video deduplication collapses 60-frame clips to ~10 keyframes** — 13-45% savings on local models, ~83% projected on GPT-4.1.
204
221
  11. **Model-aware OCR skip is critical** — ultra-efficient encoders like llama3.2-vision use <50 tokens/image; OCR text output would cost more, not less.
222
+ 12. **Accessibility tree routing eliminates screenshot cost entirely** for UI agents — 93-97% savings when the tree is complete; screenshot fallback is automatic when canvas/iframe nodes are detected.
223
+
224
+ ### Accessibility Tree Benchmark (GPT-4o pricing)
225
+
226
+ UI agents that send both a screenshot and an accessibility tree can route to the cheaper representation automatically.
227
+
228
+ **Real browser results — Playwright, 1280×720, live pages** (actual reported prompt_tokens):
229
+
230
+ | Page | Screenshot Tokens | Tree Tokens | Savings | Model |
231
+ |---|---|---|---|---|
232
+ | Hacker News | 750 | 192 | **74.4%** | moondream |
233
+ | Hacker News | 602 | 164 | **72.8%** | llava:7b |
234
+ | GitHub Home | 751 | 560 | **25.4%** | moondream |
235
+ | GitHub Home | 601 | 560 | **6.8%** | llava:7b |
236
+ | Wikipedia | 747 | 747 | **0%** | moondream |
237
+ | Wikipedia | 599 | 1,165 | **-94.5%** | llava:7b — tree too large |
238
+
239
+ > Wikipedia's rich navigation tree exceeded the screenshot token count on llava:7b — token0 would correctly fall back to the screenshot in this case. Hacker News (minimal DOM) shows the best real-world savings.
240
+
241
+ **Ollama model results — 7 vision models, synthetic 800×600 screenshots** (actual reported prompt_tokens):
242
+
243
+ > Synthetic screenshots: PIL-generated images with drawn UI elements (login form, todo list). Not real browser screenshots.
244
+
245
+ | Model | Screenshot Tokens | Tree Tokens | Savings | Note |
246
+ |---|---|---|---|---|
247
+ | granite3.2-vision | 10,328 | 218 | **97.9%** | High-res encoder |
248
+ | moondream | 1,500 | 168 | **88.8%** | |
249
+ | llava:7b | 1,202 | 160 | **86.7%** | |
250
+ | llava-llama3 | 1,201 | 164 | **86.3%** | |
251
+ | minicpm-v | 704 | 128 | **81.8%** | |
252
+ | gemma3:4b | 566 | 145 | **74.4%** | |
253
+ | llama3.2-vision | 46 | 130 | n/a | Ultra-efficient encoder — tree costs more; screenshot wins |
254
+
255
+ **Cloud API extrapolation** (tree tokens from Ollama measurements, screenshot tokens from published formulas, 800×600 image):
256
+
257
+ | Provider | Screenshot Tokens | Tree Tokens (avg) | Savings | At 100K calls/day, saved/mo |
258
+ |---|---|---|---|---|
259
+ | OpenAI GPT-4o | 1,530 | ~80 | **89.6%** | **~$10,282** |
260
+ | Anthropic Claude | 1,280 | ~80 | **87.6%** | **~$10,089** |
261
+
262
+ > Tree token counts are text-based and provider-agnostic (~4 chars/token). Screenshot tokens use OpenAI tile formula (85 + 170×tiles) and Anthropic pixel formula (w×h/750). Canvas/iframe nodes trigger automatic screenshot fallback — no configuration needed.
263
+
264
+ Run benchmarks:
265
+ ```bash
266
+ python -m benchmarks.bench_ax_tree # formula-based projections
267
+ python -m benchmarks.bench_ax_tree_models # all 7 Ollama vision models (synthetic)
268
+ python -m benchmarks.bench_ax_tree_real # real browser pages via Playwright
269
+ ```
205
270
 
206
271
  ### Additional Test Coverage
207
272
 
208
- Token0 includes **171 unit tests** and benchmarks across multiple suites:
273
+ Token0 includes **216 unit tests** and benchmarks across multiple suites:
209
274
 
210
275
  | Suite | Tests | What It Validates |
211
276
  |---|---|---|
@@ -222,6 +287,7 @@ Token0 includes **171 unit tests** and benchmarks across multiple suites:
222
287
  | `pdf` | 8 | PDF detection, decode, text extraction, token estimation |
223
288
  | `estimate` | 11 | /v1/estimate endpoint: single image, multiple images, remote URL skip, cost calc |
224
289
  | `langchain` | 8 | LangChain callback: import, text passthrough, image optimization, role mapping |
290
+ | `ax_tree` | 22 | AX tree serialize, opaque detection, AXUIElement format, combo routing |
225
291
 
226
292
  ---
227
293
 
@@ -369,6 +435,40 @@ response = client.chat.completions.create(
369
435
  # ~83% savings on GPT-4.1
370
436
  ```
371
437
 
438
+ ### Accessibility Tree Support (UI Agents)
439
+
440
+ If your agent captures both a screenshot and an accessibility tree, send both — Token0 picks the cheaper path automatically:
441
+
442
+ ```python
443
+ import json
444
+
445
+ # Playwright example
446
+ page = await browser.new_page()
447
+ snapshot = await page.accessibility.snapshot() # returns a dict
448
+
449
+ response = client.chat.completions.create(
450
+ model="gpt-4.1",
451
+ messages=[{
452
+ "role": "user",
453
+ "content": [
454
+ {"type": "text", "text": "What button should I click to submit the form?"},
455
+ # Screenshot fallback — only used if tree has canvas/iframe nodes
456
+ {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,..."}},
457
+ # Accessibility tree — token0 routes to this when complete
458
+ {"type": "accessibility_tree", "accessibility_tree": {
459
+ "data": snapshot,
460
+ "source": "playwright"
461
+ }},
462
+ ]
463
+ }],
464
+ extra_headers={"X-Provider-Key": "sk-..."}
465
+ )
466
+ # GitHub PR page: 2,125 tokens (screenshot) → 132 tokens (tree) — 93.8% savings
467
+ # response.token0.optimizations_applied = ["ax tree → text (1,993 tokens saved vs screenshot)"]
468
+ ```
469
+
470
+ Works with **Playwright**, **macOS AXUIElement**, **Chrome DevTools Protocol**, and pre-serialized strings. Canvas, iframe, and video elements trigger automatic screenshot fallback — no configuration needed.
471
+
372
472
  ### Streaming Support
373
473
 
374
474
  Token0 supports `stream=true` — images are optimized before streaming begins, then tokens flow word-by-word via SSE:
@@ -395,16 +495,14 @@ for chunk in stream:
395
495
 
396
496
  ### Use With LiteLLM
397
497
 
398
- Already using [LiteLLM](https://github.com/BerriAI/litellm)? Add Token0 as a callback hook — no proxy needed:
498
+ Already using [LiteLLM](https://github.com/BerriAI/litellm)?
399
499
 
400
- ```python
401
- import litellm
402
- from token0.litellm_hook import Token0Hook
500
+ **SDK** — swap the import; same arguments as `litellm.completion` / `litellm.acompletion`:
403
501
 
404
- litellm.callbacks = [Token0Hook()]
502
+ ```python
503
+ from token0.litellm_hook import completion # or acompletion
405
504
 
406
- # All your existing litellm calls now get image optimization for free
407
- response = litellm.completion(
505
+ response = completion(
408
506
  model="gpt-4.1",
409
507
  messages=[{
410
508
  "role": "user",
@@ -415,15 +513,16 @@ response = litellm.completion(
415
513
  }]
416
514
  )
417
515
 
418
- # Stats available in response metadata
419
- # response._hidden_params["metadata"]["token0"]["tokens_saved"]
516
+ # Stats: response._hidden_params["token0"]["tokens_saved"]
420
517
  ```
421
518
 
422
- Or in LiteLLM proxy `config.yaml`:
519
+ (Registering `Token0Hook()` in `litellm.callbacks` does **not** work with the SDK — LiteLLM only runs pre-call hooks inside the proxy.)
520
+
521
+ **Proxy** — in `config.yaml`:
423
522
 
424
523
  ```yaml
425
524
  litellm_settings:
426
- callbacks: ["token0.litellm_hook.Token0Hook"]
525
+ callbacks: ["token0.litellm_hook.proxy_handler_instance"]
427
526
  ```
428
527
 
429
528
  ### Use With LangChain