dsh-plugin-image-workbench 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1371 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/server/static/index.html +15 -0
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,346 +1,346 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import math
|
|
5
|
-
import re
|
|
6
|
-
from io import BytesIO
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from PIL import Image
|
|
10
|
-
|
|
11
|
-
DEFAULT_IMAGE_SIZE = (1024, 1024)
|
|
12
|
-
IMAGE_INPUT_TOKEN_MODEL = "gpt-5.4-mini"
|
|
13
|
-
|
|
14
|
-
PATCH_SIZE = 32
|
|
15
|
-
TILE_SIZE = 512
|
|
16
|
-
TILE_HIGH_SHORT_SIDE = 768
|
|
17
|
-
|
|
18
|
-
PATCH_1536_MODELS = (
|
|
19
|
-
"gpt-5.4-mini",
|
|
20
|
-
"gpt-5.4-nano",
|
|
21
|
-
"gpt-5-mini",
|
|
22
|
-
"gpt-5-nano",
|
|
23
|
-
"gpt-5.2",
|
|
24
|
-
"gpt-5.3-codex",
|
|
25
|
-
"gpt-5-codex-mini",
|
|
26
|
-
"gpt-5.1-codex-mini",
|
|
27
|
-
"gpt-5.2-codex",
|
|
28
|
-
"gpt-5.2-chat-latest",
|
|
29
|
-
"o4-mini",
|
|
30
|
-
"gpt-4.1-mini",
|
|
31
|
-
"gpt-4.1-nano",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
PATCH_MULTIPLIERS = {
|
|
35
|
-
"gpt-5.4-mini": 1.62,
|
|
36
|
-
"gpt-5.4-nano": 2.46,
|
|
37
|
-
"gpt-5-mini": 1.62,
|
|
38
|
-
"gpt-5-nano": 2.46,
|
|
39
|
-
"gpt-4.1-mini": 1.62,
|
|
40
|
-
"gpt-4.1-nano": 2.46,
|
|
41
|
-
"o4-mini": 1.72,
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _model_name(model: str) -> str:
|
|
46
|
-
return str(model or "").strip().lower()
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def image_size_from_bytes(data: bytes) -> tuple[int, int] | None:
|
|
50
|
-
if not data:
|
|
51
|
-
return None
|
|
52
|
-
try:
|
|
53
|
-
with Image.open(BytesIO(data)) as image:
|
|
54
|
-
width, height = image.size
|
|
55
|
-
except Exception:
|
|
56
|
-
return None
|
|
57
|
-
if width <= 0 or height <= 0:
|
|
58
|
-
return None
|
|
59
|
-
return int(width), int(height)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _decode_data_url(value: str) -> bytes:
|
|
63
|
-
text = str(value or "").strip()
|
|
64
|
-
payload = text.split(",", 1)[1] if text.startswith("data:") and "," in text else text
|
|
65
|
-
return base64.b64decode(payload)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def image_size_from_data_url(value: str) -> tuple[int, int] | None:
|
|
69
|
-
try:
|
|
70
|
-
return image_size_from_bytes(_decode_data_url(value))
|
|
71
|
-
except Exception:
|
|
72
|
-
return None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def parse_image_size(size: object, default: tuple[int, int] = DEFAULT_IMAGE_SIZE) -> tuple[int, int]:
|
|
76
|
-
if isinstance(size, (tuple, list)) and len(size) >= 2:
|
|
77
|
-
try:
|
|
78
|
-
width = int(size[0])
|
|
79
|
-
height = int(size[1])
|
|
80
|
-
if width > 0 and height > 0:
|
|
81
|
-
return width, height
|
|
82
|
-
except (TypeError, ValueError):
|
|
83
|
-
pass
|
|
84
|
-
match = re.search(r"(\d{2,5})\D+(\d{2,5})", str(size or ""))
|
|
85
|
-
if not match:
|
|
86
|
-
return default
|
|
87
|
-
width, height = int(match.group(1)), int(match.group(2))
|
|
88
|
-
return (width, height) if width > 0 and height > 0 else default
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _patch_count(width: float, height: float) -> int:
|
|
92
|
-
return math.ceil(width / PATCH_SIZE) * math.ceil(height / PATCH_SIZE)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _patch_multiplier(model: str) -> float:
|
|
96
|
-
name = _model_name(model)
|
|
97
|
-
for prefix, multiplier in PATCH_MULTIPLIERS.items():
|
|
98
|
-
if name.startswith(prefix):
|
|
99
|
-
return multiplier
|
|
100
|
-
return 1.0
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _patch_limits(model: str, detail: str) -> tuple[int, int] | None:
|
|
104
|
-
name = _model_name(model)
|
|
105
|
-
if any(name.startswith(prefix) for prefix in PATCH_1536_MODELS):
|
|
106
|
-
return 1536, 2048
|
|
107
|
-
if name.startswith("gpt-5.5"):
|
|
108
|
-
return (10000, 6000) if detail in {"auto", "original"} else (2500, 2048)
|
|
109
|
-
if name.startswith("gpt-5.4"):
|
|
110
|
-
return (10000, 6000) if detail == "original" else (2500, 2048)
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _patch_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
115
|
-
multiplier = _patch_multiplier(model)
|
|
116
|
-
if detail == "low":
|
|
117
|
-
return math.ceil(256 * multiplier)
|
|
118
|
-
|
|
119
|
-
limits = _patch_limits(model, detail)
|
|
120
|
-
if limits is None:
|
|
121
|
-
return 0
|
|
122
|
-
patch_budget, max_dimension = limits
|
|
123
|
-
scale = min(1.0, max_dimension / max(width, height))
|
|
124
|
-
resized_width = width * scale
|
|
125
|
-
resized_height = height * scale
|
|
126
|
-
|
|
127
|
-
if _patch_count(resized_width, resized_height) > patch_budget:
|
|
128
|
-
shrink_factor = math.sqrt((PATCH_SIZE * PATCH_SIZE * patch_budget) / (resized_width * resized_height))
|
|
129
|
-
width_units = resized_width * shrink_factor / PATCH_SIZE
|
|
130
|
-
height_units = resized_height * shrink_factor / PATCH_SIZE
|
|
131
|
-
adjusted_shrink_factor = shrink_factor * min(
|
|
132
|
-
math.floor(width_units) / width_units if width_units else 1,
|
|
133
|
-
math.floor(height_units) / height_units if height_units else 1,
|
|
134
|
-
)
|
|
135
|
-
resized_width *= adjusted_shrink_factor
|
|
136
|
-
resized_height *= adjusted_shrink_factor
|
|
137
|
-
|
|
138
|
-
tokens = min(_patch_count(max(1, resized_width), max(1, resized_height)), patch_budget)
|
|
139
|
-
return math.ceil(tokens * multiplier)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def _tile_rates(model: str) -> tuple[int, int]:
|
|
143
|
-
name = _model_name(model)
|
|
144
|
-
if name in {"gpt-5", "gpt-5-chat-latest"}:
|
|
145
|
-
return 70, 140
|
|
146
|
-
if name.startswith("gpt-4o-mini"):
|
|
147
|
-
return 2833, 5667
|
|
148
|
-
if name.startswith(("o1", "o1-pro", "o3")):
|
|
149
|
-
return 75, 150
|
|
150
|
-
if name.startswith("computer-use-preview"):
|
|
151
|
-
return 65, 129
|
|
152
|
-
return 85, 170
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _tile_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
156
|
-
base_tokens, tile_tokens = _tile_rates(model)
|
|
157
|
-
if detail == "low":
|
|
158
|
-
return base_tokens
|
|
159
|
-
|
|
160
|
-
scale = min(1.0, 2048 / max(width, height))
|
|
161
|
-
resized_width = width * scale
|
|
162
|
-
resized_height = height * scale
|
|
163
|
-
short_side = min(resized_width, resized_height)
|
|
164
|
-
if short_side > 0:
|
|
165
|
-
scale = TILE_HIGH_SHORT_SIDE / short_side
|
|
166
|
-
resized_width *= scale
|
|
167
|
-
resized_height *= scale
|
|
168
|
-
|
|
169
|
-
tiles = math.ceil(resized_width / TILE_SIZE) * math.ceil(resized_height / TILE_SIZE)
|
|
170
|
-
return base_tokens + tiles * tile_tokens
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def count_image_input_tokens(
|
|
174
|
-
width: int,
|
|
175
|
-
height: int,
|
|
176
|
-
model: str,
|
|
177
|
-
detail: str = "auto",
|
|
178
|
-
input_fidelity: str = "low",
|
|
179
|
-
) -> int:
|
|
180
|
-
if width <= 0 or height <= 0:
|
|
181
|
-
return 0
|
|
182
|
-
detail = str(detail or "auto").strip().lower() or "auto"
|
|
183
|
-
return _patch_tokens(width, height, IMAGE_INPUT_TOKEN_MODEL, detail)
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def _part_size(part: dict[str, Any]) -> tuple[int, int] | None:
|
|
187
|
-
try:
|
|
188
|
-
width = int(part.get("width") or 0)
|
|
189
|
-
height = int(part.get("height") or 0)
|
|
190
|
-
except (TypeError, ValueError):
|
|
191
|
-
width = height = 0
|
|
192
|
-
if width > 0 and height > 0:
|
|
193
|
-
return width, height
|
|
194
|
-
|
|
195
|
-
data = part.get("data")
|
|
196
|
-
if isinstance(data, (bytes, bytearray)):
|
|
197
|
-
return image_size_from_bytes(bytes(data))
|
|
198
|
-
|
|
199
|
-
image_url = part.get("image_url")
|
|
200
|
-
if isinstance(image_url, dict):
|
|
201
|
-
image_url = image_url.get("url") or image_url.get("image_url")
|
|
202
|
-
if isinstance(image_url, str) and image_url.startswith("data:"):
|
|
203
|
-
return image_size_from_data_url(image_url)
|
|
204
|
-
|
|
205
|
-
source = part.get("source")
|
|
206
|
-
if isinstance(source, dict) and str(source.get("type") or "") == "base64":
|
|
207
|
-
try:
|
|
208
|
-
return image_size_from_bytes(base64.b64decode(str(source.get("data") or "")))
|
|
209
|
-
except Exception:
|
|
210
|
-
return None
|
|
211
|
-
return None
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def count_image_content_tokens(content: object, model: str, default_detail: str = "auto") -> int:
|
|
215
|
-
if not isinstance(content, list):
|
|
216
|
-
return 0
|
|
217
|
-
total = 0
|
|
218
|
-
for part in content:
|
|
219
|
-
if not isinstance(part, dict):
|
|
220
|
-
continue
|
|
221
|
-
part_type = str(part.get("type") or "").strip()
|
|
222
|
-
if part_type not in {"image", "image_url", "input_image"} and not part.get("source"):
|
|
223
|
-
continue
|
|
224
|
-
size = _part_size(part)
|
|
225
|
-
if not size:
|
|
226
|
-
continue
|
|
227
|
-
total += count_image_input_tokens(
|
|
228
|
-
size[0],
|
|
229
|
-
size[1],
|
|
230
|
-
model,
|
|
231
|
-
str(part.get("detail") or default_detail or "auto"),
|
|
232
|
-
str(part.get("input_fidelity") or part.get("inputFidelity") or "low"),
|
|
233
|
-
)
|
|
234
|
-
return total
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
def count_image_inputs_tokens(images: object, model: str, default_detail: str = "auto") -> int:
|
|
238
|
-
if not images:
|
|
239
|
-
return 0
|
|
240
|
-
total = 0
|
|
241
|
-
entries = images if isinstance(images, list) else [images]
|
|
242
|
-
for image in entries:
|
|
243
|
-
data = image[0] if isinstance(image, tuple) and image else image
|
|
244
|
-
if not isinstance(data, (bytes, bytearray)):
|
|
245
|
-
continue
|
|
246
|
-
size = image_size_from_bytes(bytes(data))
|
|
247
|
-
if size:
|
|
248
|
-
total += count_image_input_tokens(size[0], size[1], model, default_detail)
|
|
249
|
-
return total
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def count_generated_image_tokens(width: int, height: int, quality: str = "auto") -> int:
|
|
253
|
-
patches = _patch_count(width, height)
|
|
254
|
-
quality = str(quality or "auto").strip().lower()
|
|
255
|
-
if quality == "low":
|
|
256
|
-
return math.ceil(patches * 17 / 64)
|
|
257
|
-
if quality in {"high", "hd"}:
|
|
258
|
-
return math.ceil(patches * 65 / 16)
|
|
259
|
-
return math.ceil(patches * 33 / 32)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def count_image_output_tokens(size: object = None, quality: str = "auto", count: int = 1) -> int:
|
|
263
|
-
width, height = parse_image_size(size)
|
|
264
|
-
return max(0, int(count or 0)) * count_generated_image_tokens(width, height, quality)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def count_image_output_items_tokens(
|
|
268
|
-
items: object,
|
|
269
|
-
size: object = None,
|
|
270
|
-
quality: str = "auto",
|
|
271
|
-
) -> int:
|
|
272
|
-
if not isinstance(items, list) or not items:
|
|
273
|
-
return 0
|
|
274
|
-
fallback_size = parse_image_size(size)
|
|
275
|
-
total = 0
|
|
276
|
-
for item in items:
|
|
277
|
-
image_size = None
|
|
278
|
-
if isinstance(item, dict):
|
|
279
|
-
b64_json = str(item.get("b64_json") or "").strip()
|
|
280
|
-
if b64_json:
|
|
281
|
-
try:
|
|
282
|
-
image_size = image_size_from_bytes(base64.b64decode(b64_json))
|
|
283
|
-
except Exception:
|
|
284
|
-
image_size = None
|
|
285
|
-
width, height = image_size or fallback_size
|
|
286
|
-
total += count_generated_image_tokens(width, height, quality)
|
|
287
|
-
return total
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def token_usage(
|
|
291
|
-
input_text_tokens: int = 0,
|
|
292
|
-
input_image_tokens: int = 0,
|
|
293
|
-
output_text_tokens: int = 0,
|
|
294
|
-
output_image_tokens: int = 0,
|
|
295
|
-
) -> dict[str, Any]:
|
|
296
|
-
input_tokens = max(0, int(input_text_tokens or 0)) + max(0, int(input_image_tokens or 0))
|
|
297
|
-
output_tokens = max(0, int(output_text_tokens or 0)) + max(0, int(output_image_tokens or 0))
|
|
298
|
-
return {
|
|
299
|
-
"input_tokens": input_tokens,
|
|
300
|
-
"output_tokens": output_tokens,
|
|
301
|
-
"total_tokens": input_tokens + output_tokens,
|
|
302
|
-
"input_tokens_details": {
|
|
303
|
-
"text_tokens": max(0, int(input_text_tokens or 0)),
|
|
304
|
-
"image_tokens": max(0, int(input_image_tokens or 0)),
|
|
305
|
-
"cached_tokens": 0,
|
|
306
|
-
},
|
|
307
|
-
"output_tokens_details": {
|
|
308
|
-
"text_tokens": max(0, int(output_text_tokens or 0)),
|
|
309
|
-
"image_tokens": max(0, int(output_image_tokens or 0)),
|
|
310
|
-
"reasoning_tokens": 0,
|
|
311
|
-
},
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def image_usage(
|
|
316
|
-
input_text_tokens: int = 0,
|
|
317
|
-
input_image_tokens: int = 0,
|
|
318
|
-
output_tokens: int = 0,
|
|
319
|
-
) -> dict[str, Any]:
|
|
320
|
-
return token_usage(
|
|
321
|
-
input_text_tokens=input_text_tokens,
|
|
322
|
-
input_image_tokens=input_image_tokens,
|
|
323
|
-
output_image_tokens=output_tokens,
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def chat_usage_from_image_usage(usage: dict[str, Any]) -> dict[str, Any]:
|
|
328
|
-
input_tokens = int(usage.get("input_tokens") or 0)
|
|
329
|
-
output_tokens = int(usage.get("output_tokens") or 0)
|
|
330
|
-
input_details = usage.get("input_tokens_details") if isinstance(usage.get("input_tokens_details"), dict) else {}
|
|
331
|
-
output_details = usage.get("output_tokens_details") if isinstance(usage.get("output_tokens_details"), dict) else {}
|
|
332
|
-
return {
|
|
333
|
-
"prompt_tokens": input_tokens,
|
|
334
|
-
"completion_tokens": output_tokens,
|
|
335
|
-
"total_tokens": input_tokens + output_tokens,
|
|
336
|
-
"prompt_tokens_details": {
|
|
337
|
-
"text_tokens": int(input_details.get("text_tokens") or 0),
|
|
338
|
-
"image_tokens": int(input_details.get("image_tokens") or 0),
|
|
339
|
-
"cached_tokens": int(input_details.get("cached_tokens") or 0),
|
|
340
|
-
},
|
|
341
|
-
"completion_tokens_details": {
|
|
342
|
-
"text_tokens": int(output_details.get("text_tokens") or 0),
|
|
343
|
-
"image_tokens": int(output_details.get("image_tokens") or 0),
|
|
344
|
-
"reasoning_tokens": int(output_details.get("reasoning_tokens") or 0),
|
|
345
|
-
},
|
|
346
|
-
}
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import math
|
|
5
|
+
import re
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from PIL import Image
|
|
10
|
+
|
|
11
|
+
DEFAULT_IMAGE_SIZE = (1024, 1024)
|
|
12
|
+
IMAGE_INPUT_TOKEN_MODEL = "gpt-5.4-mini"
|
|
13
|
+
|
|
14
|
+
PATCH_SIZE = 32
|
|
15
|
+
TILE_SIZE = 512
|
|
16
|
+
TILE_HIGH_SHORT_SIDE = 768
|
|
17
|
+
|
|
18
|
+
PATCH_1536_MODELS = (
|
|
19
|
+
"gpt-5.4-mini",
|
|
20
|
+
"gpt-5.4-nano",
|
|
21
|
+
"gpt-5-mini",
|
|
22
|
+
"gpt-5-nano",
|
|
23
|
+
"gpt-5.2",
|
|
24
|
+
"gpt-5.3-codex",
|
|
25
|
+
"gpt-5-codex-mini",
|
|
26
|
+
"gpt-5.1-codex-mini",
|
|
27
|
+
"gpt-5.2-codex",
|
|
28
|
+
"gpt-5.2-chat-latest",
|
|
29
|
+
"o4-mini",
|
|
30
|
+
"gpt-4.1-mini",
|
|
31
|
+
"gpt-4.1-nano",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
PATCH_MULTIPLIERS = {
|
|
35
|
+
"gpt-5.4-mini": 1.62,
|
|
36
|
+
"gpt-5.4-nano": 2.46,
|
|
37
|
+
"gpt-5-mini": 1.62,
|
|
38
|
+
"gpt-5-nano": 2.46,
|
|
39
|
+
"gpt-4.1-mini": 1.62,
|
|
40
|
+
"gpt-4.1-nano": 2.46,
|
|
41
|
+
"o4-mini": 1.72,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _model_name(model: str) -> str:
|
|
46
|
+
return str(model or "").strip().lower()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def image_size_from_bytes(data: bytes) -> tuple[int, int] | None:
|
|
50
|
+
if not data:
|
|
51
|
+
return None
|
|
52
|
+
try:
|
|
53
|
+
with Image.open(BytesIO(data)) as image:
|
|
54
|
+
width, height = image.size
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
if width <= 0 or height <= 0:
|
|
58
|
+
return None
|
|
59
|
+
return int(width), int(height)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _decode_data_url(value: str) -> bytes:
|
|
63
|
+
text = str(value or "").strip()
|
|
64
|
+
payload = text.split(",", 1)[1] if text.startswith("data:") and "," in text else text
|
|
65
|
+
return base64.b64decode(payload)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def image_size_from_data_url(value: str) -> tuple[int, int] | None:
|
|
69
|
+
try:
|
|
70
|
+
return image_size_from_bytes(_decode_data_url(value))
|
|
71
|
+
except Exception:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_image_size(size: object, default: tuple[int, int] = DEFAULT_IMAGE_SIZE) -> tuple[int, int]:
|
|
76
|
+
if isinstance(size, (tuple, list)) and len(size) >= 2:
|
|
77
|
+
try:
|
|
78
|
+
width = int(size[0])
|
|
79
|
+
height = int(size[1])
|
|
80
|
+
if width > 0 and height > 0:
|
|
81
|
+
return width, height
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
pass
|
|
84
|
+
match = re.search(r"(\d{2,5})\D+(\d{2,5})", str(size or ""))
|
|
85
|
+
if not match:
|
|
86
|
+
return default
|
|
87
|
+
width, height = int(match.group(1)), int(match.group(2))
|
|
88
|
+
return (width, height) if width > 0 and height > 0 else default
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _patch_count(width: float, height: float) -> int:
|
|
92
|
+
return math.ceil(width / PATCH_SIZE) * math.ceil(height / PATCH_SIZE)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _patch_multiplier(model: str) -> float:
|
|
96
|
+
name = _model_name(model)
|
|
97
|
+
for prefix, multiplier in PATCH_MULTIPLIERS.items():
|
|
98
|
+
if name.startswith(prefix):
|
|
99
|
+
return multiplier
|
|
100
|
+
return 1.0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _patch_limits(model: str, detail: str) -> tuple[int, int] | None:
|
|
104
|
+
name = _model_name(model)
|
|
105
|
+
if any(name.startswith(prefix) for prefix in PATCH_1536_MODELS):
|
|
106
|
+
return 1536, 2048
|
|
107
|
+
if name.startswith("gpt-5.5"):
|
|
108
|
+
return (10000, 6000) if detail in {"auto", "original"} else (2500, 2048)
|
|
109
|
+
if name.startswith("gpt-5.4"):
|
|
110
|
+
return (10000, 6000) if detail == "original" else (2500, 2048)
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _patch_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
115
|
+
multiplier = _patch_multiplier(model)
|
|
116
|
+
if detail == "low":
|
|
117
|
+
return math.ceil(256 * multiplier)
|
|
118
|
+
|
|
119
|
+
limits = _patch_limits(model, detail)
|
|
120
|
+
if limits is None:
|
|
121
|
+
return 0
|
|
122
|
+
patch_budget, max_dimension = limits
|
|
123
|
+
scale = min(1.0, max_dimension / max(width, height))
|
|
124
|
+
resized_width = width * scale
|
|
125
|
+
resized_height = height * scale
|
|
126
|
+
|
|
127
|
+
if _patch_count(resized_width, resized_height) > patch_budget:
|
|
128
|
+
shrink_factor = math.sqrt((PATCH_SIZE * PATCH_SIZE * patch_budget) / (resized_width * resized_height))
|
|
129
|
+
width_units = resized_width * shrink_factor / PATCH_SIZE
|
|
130
|
+
height_units = resized_height * shrink_factor / PATCH_SIZE
|
|
131
|
+
adjusted_shrink_factor = shrink_factor * min(
|
|
132
|
+
math.floor(width_units) / width_units if width_units else 1,
|
|
133
|
+
math.floor(height_units) / height_units if height_units else 1,
|
|
134
|
+
)
|
|
135
|
+
resized_width *= adjusted_shrink_factor
|
|
136
|
+
resized_height *= adjusted_shrink_factor
|
|
137
|
+
|
|
138
|
+
tokens = min(_patch_count(max(1, resized_width), max(1, resized_height)), patch_budget)
|
|
139
|
+
return math.ceil(tokens * multiplier)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _tile_rates(model: str) -> tuple[int, int]:
|
|
143
|
+
name = _model_name(model)
|
|
144
|
+
if name in {"gpt-5", "gpt-5-chat-latest"}:
|
|
145
|
+
return 70, 140
|
|
146
|
+
if name.startswith("gpt-4o-mini"):
|
|
147
|
+
return 2833, 5667
|
|
148
|
+
if name.startswith(("o1", "o1-pro", "o3")):
|
|
149
|
+
return 75, 150
|
|
150
|
+
if name.startswith("computer-use-preview"):
|
|
151
|
+
return 65, 129
|
|
152
|
+
return 85, 170
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _tile_tokens(width: int, height: int, model: str, detail: str) -> int:
|
|
156
|
+
base_tokens, tile_tokens = _tile_rates(model)
|
|
157
|
+
if detail == "low":
|
|
158
|
+
return base_tokens
|
|
159
|
+
|
|
160
|
+
scale = min(1.0, 2048 / max(width, height))
|
|
161
|
+
resized_width = width * scale
|
|
162
|
+
resized_height = height * scale
|
|
163
|
+
short_side = min(resized_width, resized_height)
|
|
164
|
+
if short_side > 0:
|
|
165
|
+
scale = TILE_HIGH_SHORT_SIDE / short_side
|
|
166
|
+
resized_width *= scale
|
|
167
|
+
resized_height *= scale
|
|
168
|
+
|
|
169
|
+
tiles = math.ceil(resized_width / TILE_SIZE) * math.ceil(resized_height / TILE_SIZE)
|
|
170
|
+
return base_tokens + tiles * tile_tokens
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def count_image_input_tokens(
|
|
174
|
+
width: int,
|
|
175
|
+
height: int,
|
|
176
|
+
model: str,
|
|
177
|
+
detail: str = "auto",
|
|
178
|
+
input_fidelity: str = "low",
|
|
179
|
+
) -> int:
|
|
180
|
+
if width <= 0 or height <= 0:
|
|
181
|
+
return 0
|
|
182
|
+
detail = str(detail or "auto").strip().lower() or "auto"
|
|
183
|
+
return _patch_tokens(width, height, IMAGE_INPUT_TOKEN_MODEL, detail)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _part_size(part: dict[str, Any]) -> tuple[int, int] | None:
|
|
187
|
+
try:
|
|
188
|
+
width = int(part.get("width") or 0)
|
|
189
|
+
height = int(part.get("height") or 0)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
width = height = 0
|
|
192
|
+
if width > 0 and height > 0:
|
|
193
|
+
return width, height
|
|
194
|
+
|
|
195
|
+
data = part.get("data")
|
|
196
|
+
if isinstance(data, (bytes, bytearray)):
|
|
197
|
+
return image_size_from_bytes(bytes(data))
|
|
198
|
+
|
|
199
|
+
image_url = part.get("image_url")
|
|
200
|
+
if isinstance(image_url, dict):
|
|
201
|
+
image_url = image_url.get("url") or image_url.get("image_url")
|
|
202
|
+
if isinstance(image_url, str) and image_url.startswith("data:"):
|
|
203
|
+
return image_size_from_data_url(image_url)
|
|
204
|
+
|
|
205
|
+
source = part.get("source")
|
|
206
|
+
if isinstance(source, dict) and str(source.get("type") or "") == "base64":
|
|
207
|
+
try:
|
|
208
|
+
return image_size_from_bytes(base64.b64decode(str(source.get("data") or "")))
|
|
209
|
+
except Exception:
|
|
210
|
+
return None
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def count_image_content_tokens(content: object, model: str, default_detail: str = "auto") -> int:
|
|
215
|
+
if not isinstance(content, list):
|
|
216
|
+
return 0
|
|
217
|
+
total = 0
|
|
218
|
+
for part in content:
|
|
219
|
+
if not isinstance(part, dict):
|
|
220
|
+
continue
|
|
221
|
+
part_type = str(part.get("type") or "").strip()
|
|
222
|
+
if part_type not in {"image", "image_url", "input_image"} and not part.get("source"):
|
|
223
|
+
continue
|
|
224
|
+
size = _part_size(part)
|
|
225
|
+
if not size:
|
|
226
|
+
continue
|
|
227
|
+
total += count_image_input_tokens(
|
|
228
|
+
size[0],
|
|
229
|
+
size[1],
|
|
230
|
+
model,
|
|
231
|
+
str(part.get("detail") or default_detail or "auto"),
|
|
232
|
+
str(part.get("input_fidelity") or part.get("inputFidelity") or "low"),
|
|
233
|
+
)
|
|
234
|
+
return total
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def count_image_inputs_tokens(images: object, model: str, default_detail: str = "auto") -> int:
|
|
238
|
+
if not images:
|
|
239
|
+
return 0
|
|
240
|
+
total = 0
|
|
241
|
+
entries = images if isinstance(images, list) else [images]
|
|
242
|
+
for image in entries:
|
|
243
|
+
data = image[0] if isinstance(image, tuple) and image else image
|
|
244
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
245
|
+
continue
|
|
246
|
+
size = image_size_from_bytes(bytes(data))
|
|
247
|
+
if size:
|
|
248
|
+
total += count_image_input_tokens(size[0], size[1], model, default_detail)
|
|
249
|
+
return total
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def count_generated_image_tokens(width: int, height: int, quality: str = "auto") -> int:
|
|
253
|
+
patches = _patch_count(width, height)
|
|
254
|
+
quality = str(quality or "auto").strip().lower()
|
|
255
|
+
if quality == "low":
|
|
256
|
+
return math.ceil(patches * 17 / 64)
|
|
257
|
+
if quality in {"high", "hd"}:
|
|
258
|
+
return math.ceil(patches * 65 / 16)
|
|
259
|
+
return math.ceil(patches * 33 / 32)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def count_image_output_tokens(size: object = None, quality: str = "auto", count: int = 1) -> int:
|
|
263
|
+
width, height = parse_image_size(size)
|
|
264
|
+
return max(0, int(count or 0)) * count_generated_image_tokens(width, height, quality)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def count_image_output_items_tokens(
|
|
268
|
+
items: object,
|
|
269
|
+
size: object = None,
|
|
270
|
+
quality: str = "auto",
|
|
271
|
+
) -> int:
|
|
272
|
+
if not isinstance(items, list) or not items:
|
|
273
|
+
return 0
|
|
274
|
+
fallback_size = parse_image_size(size)
|
|
275
|
+
total = 0
|
|
276
|
+
for item in items:
|
|
277
|
+
image_size = None
|
|
278
|
+
if isinstance(item, dict):
|
|
279
|
+
b64_json = str(item.get("b64_json") or "").strip()
|
|
280
|
+
if b64_json:
|
|
281
|
+
try:
|
|
282
|
+
image_size = image_size_from_bytes(base64.b64decode(b64_json))
|
|
283
|
+
except Exception:
|
|
284
|
+
image_size = None
|
|
285
|
+
width, height = image_size or fallback_size
|
|
286
|
+
total += count_generated_image_tokens(width, height, quality)
|
|
287
|
+
return total
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def token_usage(
|
|
291
|
+
input_text_tokens: int = 0,
|
|
292
|
+
input_image_tokens: int = 0,
|
|
293
|
+
output_text_tokens: int = 0,
|
|
294
|
+
output_image_tokens: int = 0,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
input_tokens = max(0, int(input_text_tokens or 0)) + max(0, int(input_image_tokens or 0))
|
|
297
|
+
output_tokens = max(0, int(output_text_tokens or 0)) + max(0, int(output_image_tokens or 0))
|
|
298
|
+
return {
|
|
299
|
+
"input_tokens": input_tokens,
|
|
300
|
+
"output_tokens": output_tokens,
|
|
301
|
+
"total_tokens": input_tokens + output_tokens,
|
|
302
|
+
"input_tokens_details": {
|
|
303
|
+
"text_tokens": max(0, int(input_text_tokens or 0)),
|
|
304
|
+
"image_tokens": max(0, int(input_image_tokens or 0)),
|
|
305
|
+
"cached_tokens": 0,
|
|
306
|
+
},
|
|
307
|
+
"output_tokens_details": {
|
|
308
|
+
"text_tokens": max(0, int(output_text_tokens or 0)),
|
|
309
|
+
"image_tokens": max(0, int(output_image_tokens or 0)),
|
|
310
|
+
"reasoning_tokens": 0,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def image_usage(
|
|
316
|
+
input_text_tokens: int = 0,
|
|
317
|
+
input_image_tokens: int = 0,
|
|
318
|
+
output_tokens: int = 0,
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
return token_usage(
|
|
321
|
+
input_text_tokens=input_text_tokens,
|
|
322
|
+
input_image_tokens=input_image_tokens,
|
|
323
|
+
output_image_tokens=output_tokens,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def chat_usage_from_image_usage(usage: dict[str, Any]) -> dict[str, Any]:
|
|
328
|
+
input_tokens = int(usage.get("input_tokens") or 0)
|
|
329
|
+
output_tokens = int(usage.get("output_tokens") or 0)
|
|
330
|
+
input_details = usage.get("input_tokens_details") if isinstance(usage.get("input_tokens_details"), dict) else {}
|
|
331
|
+
output_details = usage.get("output_tokens_details") if isinstance(usage.get("output_tokens_details"), dict) else {}
|
|
332
|
+
return {
|
|
333
|
+
"prompt_tokens": input_tokens,
|
|
334
|
+
"completion_tokens": output_tokens,
|
|
335
|
+
"total_tokens": input_tokens + output_tokens,
|
|
336
|
+
"prompt_tokens_details": {
|
|
337
|
+
"text_tokens": int(input_details.get("text_tokens") or 0),
|
|
338
|
+
"image_tokens": int(input_details.get("image_tokens") or 0),
|
|
339
|
+
"cached_tokens": int(input_details.get("cached_tokens") or 0),
|
|
340
|
+
},
|
|
341
|
+
"completion_tokens_details": {
|
|
342
|
+
"text_tokens": int(output_details.get("text_tokens") or 0),
|
|
343
|
+
"image_tokens": int(output_details.get("image_tokens") or 0),
|
|
344
|
+
"reasoning_tokens": int(output_details.get("reasoning_tokens") or 0),
|
|
345
|
+
},
|
|
346
|
+
}
|