dsh-plugin-image-workbench 0.0.0-stage → 1.1.5

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +256 -0
  3. package/README.md +256 -2
  4. package/client.js +241 -0
  5. package/cordis.patch.yml +14 -0
  6. package/host.js +641 -0
  7. package/install.mjs +387 -0
  8. package/package.json +57 -4
  9. package/scripts/vendor-snapshot.py +154 -0
  10. package/server/access.py +26 -0
  11. package/server/app.py +1329 -0
  12. package/server/catalog.json +1 -0
  13. package/server/catalog.py +147 -0
  14. package/server/composer.py +391 -0
  15. package/server/drafts.py +158 -0
  16. package/server/history.py +575 -0
  17. package/server/imaging.py +172 -0
  18. package/server/pool.py +1335 -0
  19. package/server/pool_admin.py +921 -0
  20. package/server/pool_service.py +1073 -0
  21. package/server/requirements.txt +6 -0
  22. package/server/smoke_check.py +402 -0
  23. package/server/static/access.html +202 -0
  24. package/server/static/index.html +4725 -0
  25. package/vendor/chatgpt2api/README.md +13 -0
  26. package/vendor/chatgpt2api/SOURCE.json +11 -0
  27. package/vendor/chatgpt2api/runtime/uv.exe +0 -0
  28. package/vendor/chatgpt2api/source/.env.example +82 -0
  29. package/vendor/chatgpt2api/source/.python-version +1 -0
  30. package/vendor/chatgpt2api/source/LICENSE +21 -0
  31. package/vendor/chatgpt2api/source/README.md +391 -0
  32. package/vendor/chatgpt2api/source/VERSION +1 -0
  33. package/vendor/chatgpt2api/source/api/__init__.py +2 -0
  34. package/vendor/chatgpt2api/source/api/accounts.py +537 -0
  35. package/vendor/chatgpt2api/source/api/ai.py +217 -0
  36. package/vendor/chatgpt2api/source/api/app.py +62 -0
  37. package/vendor/chatgpt2api/source/api/errors.py +46 -0
  38. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
  39. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
  40. package/vendor/chatgpt2api/source/api/support.py +134 -0
  41. package/vendor/chatgpt2api/source/api/system.py +365 -0
  42. package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
  43. package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
  44. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
  45. package/vendor/chatgpt2api/source/config.default.json +108 -0
  46. package/vendor/chatgpt2api/source/main.py +9 -0
  47. package/vendor/chatgpt2api/source/pyproject.toml +27 -0
  48. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
  49. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
  50. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
  51. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
  52. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
  53. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
  54. package/vendor/chatgpt2api/source/services/__init__.py +0 -0
  55. package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
  56. package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
  57. package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
  58. package/vendor/chatgpt2api/source/services/config.py +667 -0
  59. package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
  60. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
  61. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
  62. package/vendor/chatgpt2api/source/services/image_service.py +376 -0
  63. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
  64. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
  65. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
  66. package/vendor/chatgpt2api/source/services/log_service.py +337 -0
  67. package/vendor/chatgpt2api/source/services/model_service.py +182 -0
  68. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
  69. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
  70. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
  71. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
  72. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
  73. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
  74. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
  75. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
  76. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
  77. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
  78. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
  79. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
  80. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
  81. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
  82. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
  83. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
  84. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
  85. package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
  86. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
  87. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
  88. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
  89. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
  90. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
  91. package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
  92. package/vendor/chatgpt2api/source/utils/helper.py +469 -0
  93. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
  94. package/vendor/chatgpt2api/source/utils/log.py +110 -0
  95. package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
  96. package/vendor/chatgpt2api/source/utils/pow.py +207 -0
  97. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
  98. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
  99. package/vendor/chatgpt2api/source/uv.lock +904 -0
  100. package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
  101. package/vendor/chatgpt2api/source/web/components.json +24 -0
  102. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
  103. package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
  104. package/vendor/chatgpt2api/source/web/package.json +58 -0
  105. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
  106. package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
  107. package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
  108. package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
  109. package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
  110. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
  111. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
  112. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
  113. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
  114. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
  115. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
  116. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
  117. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
  118. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
  119. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
  120. package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
  121. package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
  122. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
  123. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
  124. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
  125. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
  126. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
  127. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
  128. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
  129. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
  130. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
  131. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
  133. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
  134. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
  135. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
  136. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
  137. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
  138. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
  139. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
  140. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
  141. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
  142. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
  143. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
  144. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
  145. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
  146. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
  147. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
  148. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
  149. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
  150. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
  151. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
  152. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
  153. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
  154. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
  155. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
  156. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
  157. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
  158. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
  159. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
  160. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
  161. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
  162. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
  163. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
  164. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
  165. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
  166. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
  167. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
  168. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
  169. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
  170. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
  171. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
  172. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
  173. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
  174. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
  175. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
  176. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
  177. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
  178. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
  179. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
  180. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
  181. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
  182. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
  183. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
  184. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
  185. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
@@ -0,0 +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
+ }
@@ -0,0 +1,110 @@
1
+ import base64
2
+ import binascii
3
+ import json
4
+ import logging
5
+ import re
6
+ from typing import Any
7
+
8
+
9
+ class Logger:
10
+ _DATA_URL_RE = re.compile(r"data:image/[^;]+;base64,[A-Za-z0-9+/=]+")
11
+ _JSON_B64_RE = re.compile(r'("b64_json"\s*:\s*")([A-Za-z0-9+/=]+)(")')
12
+
13
+ def __init__(self, name: str = "chatgpt2api") -> None:
14
+ self._logger = logging.getLogger(name)
15
+ if not self._logger.handlers:
16
+ handler = logging.StreamHandler()
17
+ handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
18
+ self._logger.addHandler(handler)
19
+ self._logger.setLevel(logging.DEBUG)
20
+ self._logger.propagate = False
21
+
22
+ def _enabled(self, level: str) -> bool:
23
+ try:
24
+ from services.config import config
25
+ levels = set(config.log_levels)
26
+ except Exception:
27
+ levels = set()
28
+ return level in (levels or {"info", "warning", "error"})
29
+
30
+ def _mask_string(self, value: str, keep: int = 10) -> str:
31
+ if len(value) <= keep:
32
+ return value
33
+ return value[:keep] + "..."
34
+
35
+ def _mask_base64(self, value: str) -> str:
36
+ if value.startswith("data:") and ";base64," in value:
37
+ header, _, data = value.partition(",")
38
+ return f"{header},{self._mask_string(data, 24)} (base64 len={len(data)})"
39
+ return f"{self._mask_string(value, 24)} (base64 len={len(value)})"
40
+
41
+ def _is_base64_string(self, value: str) -> bool:
42
+ if len(value) < 64 or len(value) % 4 != 0:
43
+ return False
44
+ if not any(char in value for char in "+/="):
45
+ return False
46
+ try:
47
+ base64.b64decode(value, validate=True)
48
+ return True
49
+ except (binascii.Error, ValueError):
50
+ return False
51
+
52
+ def _sanitize_string(self, value: str) -> str:
53
+ stripped = value.strip()
54
+ if stripped.startswith("data:") and ";base64," in stripped:
55
+ return self._mask_base64(stripped)
56
+ if self._is_base64_string(stripped):
57
+ return self._mask_base64(stripped)
58
+ sanitized = self._DATA_URL_RE.sub(lambda match: self._mask_base64(match.group(0)), value)
59
+ sanitized = self._JSON_B64_RE.sub(
60
+ lambda match: f'{match.group(1)}{self._mask_base64(match.group(2))}{match.group(3)}',
61
+ sanitized,
62
+ )
63
+ if sanitized != value:
64
+ return sanitized
65
+ return value
66
+
67
+ def _sanitize(self, value: Any) -> Any:
68
+ if isinstance(value, dict):
69
+ sanitized = {}
70
+ for key, item in value.items():
71
+ lowered_key = key.lower()
72
+ if isinstance(item, str) and ("token" in lowered_key or lowered_key == "dx"):
73
+ sanitized[key] = self._mask_string(item)
74
+ elif isinstance(item, str) and ("base64" in lowered_key or lowered_key == "b64_json"):
75
+ sanitized[key] = self._mask_base64(item)
76
+ else:
77
+ sanitized[key] = self._sanitize(item)
78
+ return sanitized
79
+ if isinstance(value, list):
80
+ return [self._sanitize(item) for item in value]
81
+ if isinstance(value, tuple):
82
+ return tuple(self._sanitize(item) for item in value)
83
+ if isinstance(value, str):
84
+ return self._sanitize_string(value)
85
+ return value
86
+
87
+ def _message(self, value: Any) -> str:
88
+ sanitized = self._sanitize(value)
89
+ if isinstance(sanitized, str):
90
+ return sanitized
91
+ return json.dumps(sanitized, ensure_ascii=False, default=str)
92
+
93
+ def debug(self, message: Any) -> None:
94
+ if self._enabled("debug"):
95
+ self._logger.debug(self._message(message))
96
+
97
+ def info(self, message: Any) -> None:
98
+ if self._enabled("info"):
99
+ self._logger.info(self._message(message))
100
+
101
+ def warning(self, message: Any) -> None:
102
+ if self._enabled("warning"):
103
+ self._logger.warning(self._message(message))
104
+
105
+ def error(self, message: Any) -> None:
106
+ if self._enabled("error"):
107
+ self._logger.error(self._message(message))
108
+
109
+
110
+ logger = Logger()
@@ -0,0 +1,19 @@
1
+ """PKCE (Proof Key for Code Exchange) 工具函数"""
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import hashlib
6
+ import secrets
7
+
8
+
9
+ def generate_pkce() -> tuple[str, str]:
10
+ """生成 PKCE code_verifier 与对应的 code_challenge(S256)。
11
+
12
+ Returns:
13
+ (code_verifier, code_challenge) 元组
14
+ """
15
+ code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii")
16
+ code_challenge = base64.urlsafe_b64encode(
17
+ hashlib.sha256(code_verifier.encode("ascii")).digest()
18
+ ).rstrip(b"=").decode("ascii")
19
+ return code_verifier, code_challenge
@@ -0,0 +1,207 @@
1
+ import hashlib
2
+ import json
3
+ import random
4
+ import re
5
+ import time
6
+ from datetime import datetime, timedelta, timezone
7
+ from html.parser import HTMLParser
8
+ from typing import Any, Sequence
9
+
10
+ import pybase64
11
+
12
+ DEFAULT_POW_SCRIPT = "https://chatgpt.com/backend-api/sentinel/sdk.js"
13
+ from utils.helper import new_uuid
14
+
15
+
16
+ CORES = [8, 16, 24, 32]
17
+ DOCUMENT_KEYS = ["__reactContainer$fzelfjyxej8", "_reactListening5dehydibo78", "location"]
18
+ SCREEN_RESOLUTIONS = [[1920, 1080], [1440, 900], [2560, 1440], [3840, 2160]]
19
+
20
+
21
+ class ScriptSrcParser(HTMLParser):
22
+ def __init__(self) -> None:
23
+ super().__init__()
24
+ self.script_sources: list[str] = []
25
+ self.data_build = ""
26
+
27
+ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
28
+ if tag != "script":
29
+ return
30
+ attrs_dict = dict(attrs)
31
+ src = attrs_dict.get("src")
32
+ if not src:
33
+ return
34
+ self.script_sources.append(src)
35
+ match = re.search(r"c/[^/]*/_", src)
36
+ if match:
37
+ self.data_build = match.group(0)
38
+
39
+
40
+ def parse_pow_resources(html_content: str) -> tuple[list[str], str]:
41
+ parser = ScriptSrcParser()
42
+ parser.feed(html_content)
43
+ script_sources = parser.script_sources or [DEFAULT_POW_SCRIPT]
44
+ data_build = parser.data_build
45
+ if not data_build:
46
+ match = re.search(r'<html[^>]*data-build="([^"]*)"', html_content)
47
+ if match:
48
+ data_build = match.group(1)
49
+ return script_sources, data_build
50
+
51
+
52
+ def _legacy_parse_time() -> str:
53
+ now = datetime.now(timezone(timedelta(hours=-5)))
54
+ return now.strftime("%a %b %d %Y %H:%M:%S") + " GMT-0500 (Eastern Standard Time)"
55
+
56
+
57
+ def build_pow_config(
58
+ user_agent: str,
59
+ script_sources: Sequence[str] | None = None,
60
+ data_build: str = "",
61
+ ) -> list[Any]:
62
+ navigator_key = random.choice([
63
+ "registerProtocolHandler−function registerProtocolHandler() { [native code] }",
64
+ "storage−[object StorageManager]",
65
+ "locks−[object LockManager]",
66
+ "appCodeName−Mozilla",
67
+ "permissions−[object Permissions]",
68
+ "share−function share() { [native code] }",
69
+ "webdriver−false",
70
+ "managed−[object NavigatorManagedData]",
71
+ "canShare−function canShare() { [native code] }",
72
+ "vendor−Google Inc.",
73
+ "mediaDevices−[object MediaDevices]",
74
+ "vibrate−function vibrate() { [native code] }",
75
+ "storageBuckets−[object StorageBucketManager]",
76
+ "mediaCapabilities−[object MediaCapabilities]",
77
+ "cookieEnabled−true",
78
+ "virtualKeyboard−[object VirtualKeyboard]",
79
+ "product−Gecko",
80
+ "presentation−[object Presentation]",
81
+ "onLine−true",
82
+ "mimeTypes−[object MimeTypeArray]",
83
+ "credentials−[object CredentialsContainer]",
84
+ "serviceWorker−[object ServiceWorkerContainer]",
85
+ "keyboard−[object Keyboard]",
86
+ "gpu−[object GPU]",
87
+ "doNotTrack",
88
+ "serial−[object Serial]",
89
+ "pdfViewerEnabled−true",
90
+ "language−zh-CN",
91
+ "geolocation−[object Geolocation]",
92
+ "userAgentData−[object NavigatorUAData]",
93
+ "getUserMedia−function getUserMedia() { [native code] }",
94
+ "sendBeacon−function sendBeacon() { [native code] }",
95
+ "hardwareConcurrency−32",
96
+ "windowControlsOverlay−[object WindowControlsOverlay]",
97
+ ])
98
+ window_key = random.choice([
99
+ "0",
100
+ "window",
101
+ "self",
102
+ "document",
103
+ "name",
104
+ "location",
105
+ "customElements",
106
+ "history",
107
+ "navigation",
108
+ "innerWidth",
109
+ "innerHeight",
110
+ "scrollX",
111
+ "scrollY",
112
+ "visualViewport",
113
+ "screenX",
114
+ "screenY",
115
+ "outerWidth",
116
+ "outerHeight",
117
+ "devicePixelRatio",
118
+ "screen",
119
+ "chrome",
120
+ "navigator",
121
+ "onresize",
122
+ "performance",
123
+ "crypto",
124
+ "indexedDB",
125
+ "sessionStorage",
126
+ "localStorage",
127
+ "scheduler",
128
+ "alert",
129
+ "atob",
130
+ "btoa",
131
+ "fetch",
132
+ "matchMedia",
133
+ "postMessage",
134
+ "queueMicrotask",
135
+ "requestAnimationFrame",
136
+ "setInterval",
137
+ "setTimeout",
138
+ "caches",
139
+ "__NEXT_DATA__",
140
+ "__BUILD_MANIFEST",
141
+ "__NEXT_PRELOADREADY",
142
+ ])
143
+ script_source = random.choice(list(script_sources)) if script_sources else DEFAULT_POW_SCRIPT
144
+ return [
145
+ sum(random.choices(SCREEN_RESOLUTIONS, k=1)[0]),
146
+ _legacy_parse_time(),
147
+ 4294705152,
148
+ 1,
149
+ user_agent,
150
+ script_source,
151
+ data_build,
152
+ "en-US",
153
+ "en-US,es-US,en,es",
154
+ random.random(),
155
+ navigator_key,
156
+ random.choice(DOCUMENT_KEYS),
157
+ window_key,
158
+ time.perf_counter() * 1000,
159
+ new_uuid(),
160
+ "",
161
+ random.choice(CORES),
162
+ time.time() * 1000 - (time.perf_counter() * 1000),
163
+ 0, 0, 0, 0, 0, 0,
164
+ 0, # 0 = edge/chrome, 1 = firefox
165
+ ]
166
+
167
+
168
+ def _pow_generate(seed: str, difficulty: str, config: list[Any], limit: int = 500000) -> tuple[str, bool]:
169
+ target = bytes.fromhex(difficulty)
170
+ diff_len = len(difficulty) // 2
171
+ seed_bytes = seed.encode()
172
+ static_1 = (json.dumps(config[:3], separators=(",", ":"), ensure_ascii=False)[:-1] + ",").encode()
173
+ static_2 = ("," + json.dumps(config[4:9], separators=(",", ":"), ensure_ascii=False)[1:-1] + ",").encode()
174
+ static_3 = ("," + json.dumps(config[10:], separators=(",", ":"), ensure_ascii=False)[1:]).encode()
175
+ for i in range(limit):
176
+ final_json = static_1 + str(i).encode() + static_2 + str(i >> 1).encode() + static_3
177
+ encoded = pybase64.b64encode(final_json)
178
+ digest = hashlib.sha3_512(seed_bytes + encoded).digest()
179
+ if digest[:diff_len] <= target:
180
+ return encoded.decode(), True
181
+ fallback = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D" + pybase64.b64encode(f'"{seed}"'.encode()).decode()
182
+ return fallback, False
183
+
184
+
185
+ def build_legacy_requirements_token(
186
+ user_agent: str,
187
+ script_sources: Sequence[str] | None = None,
188
+ data_build: str = "",
189
+ ) -> str:
190
+ config = build_pow_config(user_agent, script_sources=script_sources, data_build=data_build)
191
+ return "gAAAAAC" + pybase64.b64encode(
192
+ json.dumps(config, separators=(",", ":"), ensure_ascii=False).encode()
193
+ ).decode()
194
+
195
+
196
+ def build_proof_token(
197
+ seed: str,
198
+ difficulty: str,
199
+ user_agent: str,
200
+ script_sources: Sequence[str] | None = None,
201
+ data_build: str = "",
202
+ ) -> str:
203
+ config = build_pow_config(user_agent, script_sources=script_sources, data_build=data_build)
204
+ answer, solved = _pow_generate(seed, difficulty, config)
205
+ if not solved:
206
+ raise RuntimeError(f"failed to solve proof token: difficulty={difficulty}")
207
+ return "gAAAAAB" + answer