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.
- token0-0.4.0/.github/workflows/publish.yml +26 -0
- {token0-0.3.2 → token0-0.4.0}/PKG-INFO +114 -15
- {token0-0.3.2 → token0-0.4.0}/README.md +112 -13
- token0-0.4.0/benchmarks/bench_ax_tree.py +359 -0
- token0-0.4.0/benchmarks/bench_ax_tree_models.py +422 -0
- token0-0.4.0/benchmarks/bench_ax_tree_real.py +686 -0
- {token0-0.3.2 → token0-0.4.0}/pyproject.toml +1 -1
- token0-0.4.0/tests/test_ax_tree.py +229 -0
- token0-0.4.0/tests/test_config.py +42 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_litellm_hook.py +70 -2
- token0-0.4.0/tests/test_ocr_guard.py +79 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_router.py +40 -6
- {token0-0.3.2 → token0-0.4.0}/token0/api/v1/chat.py +85 -22
- {token0-0.3.2 → token0-0.4.0}/token0/api/v1/estimate.py +15 -5
- {token0-0.3.2 → token0-0.4.0}/token0/config.py +18 -1
- token0-0.4.0/token0/litellm_hook.py +151 -0
- {token0-0.3.2 → token0-0.4.0}/token0/models/request.py +7 -1
- token0-0.4.0/token0/optimization/ax_tree.py +157 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/message_optimizer.py +16 -11
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/prompt_classifier.py +45 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/router.py +65 -8
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/transformer.py +35 -0
- {token0-0.3.2 → token0-0.4.0}/token0_integration.md +13 -13
- token0-0.3.2/.claude/settings.local.json +0 -48
- token0-0.3.2/token0/litellm_hook.py +0 -85
- {token0-0.3.2 → token0-0.4.0}/.env.example +0 -0
- {token0-0.3.2 → token0-0.4.0}/.github/workflows/ci.yml +0 -0
- {token0-0.3.2 → token0-0.4.0}/.gitignore +0 -0
- {token0-0.3.2 → token0-0.4.0}/Dockerfile +0 -0
- {token0-0.3.2 → token0-0.4.0}/LICENSE +0 -0
- {token0-0.3.2 → token0-0.4.0}/alembic/env.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/alembic/script.py.mako +0 -0
- {token0-0.3.2 → token0-0.4.0}/alembic.ini +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_fuzzy_cache.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_video.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/bench_video_models.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/generate_test_images.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/already_optimized.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/document_screenshot.png +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/large_photo.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/large_screenshot.png +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/document_invoice.png +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/photo_nature.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/photo_street.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/real/receipt_real.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/receipt.png +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/images/small_photo.jpg +0 -0
- {token0-0.3.2 → token0-0.4.0}/benchmarks/run.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/docker-compose.yml +0 -0
- {token0-0.3.2 → token0-0.4.0}/test_token0_litellm.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/conftest.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_analyzer.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_api.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_cache.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_estimate.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_instructor_hook.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_langchain_callback.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_pdf.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_providers.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_saliency.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_streaming.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_transformer.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/tests/test_video.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/api/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/api/auth.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/api/v1/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/api/v1/usage.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/cli.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/instructor_hook.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/langchain_callback.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/main.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/models/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/models/db.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/analyzer.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/cache.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/pdf.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/saliency.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/optimization/video.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/anthropic.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/base.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/google.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/ollama.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/providers/openai.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/static/dashboard.html +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/storage/__init__.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/storage/objects.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/storage/postgres.py +0 -0
- {token0-0.3.2 → token0-0.4.0}/token0/storage/redis.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: token0
|
|
3
|
-
Version: 0.
|
|
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 **
|
|
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 **
|
|
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)?
|
|
536
|
+
Already using [LiteLLM](https://github.com/BerriAI/litellm)?
|
|
437
537
|
|
|
438
|
-
|
|
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
|
-
|
|
540
|
+
```python
|
|
541
|
+
from token0.litellm_hook import completion # or acompletion
|
|
443
542
|
|
|
444
|
-
|
|
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
|
|
457
|
-
# response._hidden_params["metadata"]["token0"]["tokens_saved"]
|
|
554
|
+
# Stats: response._hidden_params["token0"]["tokens_saved"]
|
|
458
555
|
```
|
|
459
556
|
|
|
460
|
-
|
|
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.
|
|
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 **
|
|
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 **
|
|
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)?
|
|
498
|
+
Already using [LiteLLM](https://github.com/BerriAI/litellm)?
|
|
399
499
|
|
|
400
|
-
|
|
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
|
-
|
|
502
|
+
```python
|
|
503
|
+
from token0.litellm_hook import completion # or acompletion
|
|
405
504
|
|
|
406
|
-
|
|
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
|
|
419
|
-
# response._hidden_params["metadata"]["token0"]["tokens_saved"]
|
|
516
|
+
# Stats: response._hidden_params["token0"]["tokens_saved"]
|
|
420
517
|
```
|
|
421
518
|
|
|
422
|
-
|
|
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.
|
|
525
|
+
callbacks: ["token0.litellm_hook.proxy_handler_instance"]
|
|
427
526
|
```
|
|
428
527
|
|
|
429
528
|
### Use With LangChain
|