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

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 (186) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +258 -0
  3. package/README.md +260 -2
  4. package/client.js +241 -0
  5. package/cordis.patch.yml +14 -0
  6. package/host.js +692 -0
  7. package/install.mjs +333 -0
  8. package/package.json +58 -4
  9. package/scripts/fetch-style-images.mjs +104 -0
  10. package/scripts/vendor-snapshot.py +154 -0
  11. package/server/access.py +26 -0
  12. package/server/app.py +1329 -0
  13. package/server/catalog.json +1 -0
  14. package/server/catalog.py +147 -0
  15. package/server/composer.py +391 -0
  16. package/server/drafts.py +158 -0
  17. package/server/history.py +575 -0
  18. package/server/imaging.py +172 -0
  19. package/server/pool.py +1335 -0
  20. package/server/pool_admin.py +921 -0
  21. package/server/pool_service.py +1073 -0
  22. package/server/requirements.txt +6 -0
  23. package/server/smoke_check.py +402 -0
  24. package/server/static/access.html +202 -0
  25. package/server/static/index.html +4725 -0
  26. package/vendor/chatgpt2api/README.md +13 -0
  27. package/vendor/chatgpt2api/SOURCE.json +11 -0
  28. package/vendor/chatgpt2api/runtime/uv.exe +0 -0
  29. package/vendor/chatgpt2api/source/.env.example +82 -0
  30. package/vendor/chatgpt2api/source/.python-version +1 -0
  31. package/vendor/chatgpt2api/source/LICENSE +21 -0
  32. package/vendor/chatgpt2api/source/README.md +391 -0
  33. package/vendor/chatgpt2api/source/VERSION +1 -0
  34. package/vendor/chatgpt2api/source/api/__init__.py +2 -0
  35. package/vendor/chatgpt2api/source/api/accounts.py +537 -0
  36. package/vendor/chatgpt2api/source/api/ai.py +217 -0
  37. package/vendor/chatgpt2api/source/api/app.py +62 -0
  38. package/vendor/chatgpt2api/source/api/errors.py +46 -0
  39. package/vendor/chatgpt2api/source/api/image_inputs.py +309 -0
  40. package/vendor/chatgpt2api/source/api/image_tasks.py +120 -0
  41. package/vendor/chatgpt2api/source/api/support.py +134 -0
  42. package/vendor/chatgpt2api/source/api/system.py +365 -0
  43. package/vendor/chatgpt2api/source/assets/atlascloud.svg +1 -0
  44. package/vendor/chatgpt2api/source/assets/ipwo.png +0 -0
  45. package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -0
  46. package/vendor/chatgpt2api/source/config.default.json +108 -0
  47. package/vendor/chatgpt2api/source/main.py +9 -0
  48. package/vendor/chatgpt2api/source/pyproject.toml +27 -0
  49. package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -0
  50. package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -0
  51. package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -0
  52. package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -0
  53. package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -0
  54. package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -0
  55. package/vendor/chatgpt2api/source/services/__init__.py +0 -0
  56. package/vendor/chatgpt2api/source/services/account_service.py +1757 -0
  57. package/vendor/chatgpt2api/source/services/auth_service.py +253 -0
  58. package/vendor/chatgpt2api/source/services/backup_service.py +671 -0
  59. package/vendor/chatgpt2api/source/services/config.py +667 -0
  60. package/vendor/chatgpt2api/source/services/content_filter.py +242 -0
  61. package/vendor/chatgpt2api/source/services/cpa_service.py +315 -0
  62. package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -0
  63. package/vendor/chatgpt2api/source/services/image_service.py +376 -0
  64. package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -0
  65. package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -0
  66. package/vendor/chatgpt2api/source/services/image_task_service.py +550 -0
  67. package/vendor/chatgpt2api/source/services/log_service.py +337 -0
  68. package/vendor/chatgpt2api/source/services/model_service.py +182 -0
  69. package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -0
  70. package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -0
  71. package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -0
  72. package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -0
  73. package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -0
  74. package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -0
  75. package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -0
  76. package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -0
  77. package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -0
  78. package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -0
  79. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -0
  80. package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -0
  81. package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -0
  82. package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -0
  83. package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -0
  84. package/vendor/chatgpt2api/source/services/proxy_service.py +648 -0
  85. package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -0
  86. package/vendor/chatgpt2api/source/services/storage/base.py +38 -0
  87. package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -0
  88. package/vendor/chatgpt2api/source/services/storage/factory.py +104 -0
  89. package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -0
  90. package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -0
  91. package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -0
  92. package/vendor/chatgpt2api/source/utils/__init__.py +0 -0
  93. package/vendor/chatgpt2api/source/utils/helper.py +469 -0
  94. package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -0
  95. package/vendor/chatgpt2api/source/utils/log.py +110 -0
  96. package/vendor/chatgpt2api/source/utils/pkce.py +19 -0
  97. package/vendor/chatgpt2api/source/utils/pow.py +207 -0
  98. package/vendor/chatgpt2api/source/utils/sentinel.py +159 -0
  99. package/vendor/chatgpt2api/source/utils/turnstile.py +184 -0
  100. package/vendor/chatgpt2api/source/uv.lock +904 -0
  101. package/vendor/chatgpt2api/source/web/bun.lock +1501 -0
  102. package/vendor/chatgpt2api/source/web/components.json +24 -0
  103. package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -0
  104. package/vendor/chatgpt2api/source/web/next.config.ts +40 -0
  105. package/vendor/chatgpt2api/source/web/package.json +58 -0
  106. package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -0
  107. package/vendor/chatgpt2api/source/web/public/github.svg +1 -0
  108. package/vendor/chatgpt2api/source/web/public/next.svg +1 -0
  109. package/vendor/chatgpt2api/source/web/public/openai.svg +1 -0
  110. package/vendor/chatgpt2api/source/web/public/vercel.svg +1 -0
  111. package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -0
  112. package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -0
  113. package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -0
  114. package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -0
  115. package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -0
  116. package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -0
  117. package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -0
  118. package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -0
  119. package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -0
  120. package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -0
  121. package/vendor/chatgpt2api/source/web/src/app/favicon.ico +0 -0
  122. package/vendor/chatgpt2api/source/web/src/app/globals.css +231 -0
  123. package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -0
  124. package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -0
  125. package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -0
  126. package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -0
  127. package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -0
  128. package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -0
  129. package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -0
  130. package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -0
  131. package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -0
  132. package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -0
  133. package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -0
  134. package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -0
  135. package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -0
  136. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -0
  137. package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -0
  138. package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -0
  139. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -0
  140. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -0
  141. package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -0
  142. package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -0
  143. package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -0
  144. package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -0
  145. package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -0
  146. package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -0
  147. package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -0
  148. package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -0
  149. package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -0
  150. package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -0
  151. package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -0
  152. package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -0
  153. package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -0
  154. package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -0
  155. package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -0
  156. package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -0
  157. package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -0
  158. package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -0
  159. package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -0
  160. package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -0
  161. package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -0
  162. package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -0
  163. package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -0
  164. package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -0
  165. package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -0
  166. package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -0
  167. package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -0
  168. package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -0
  169. package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -0
  170. package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -0
  171. package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -0
  172. package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -0
  173. package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -0
  174. package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -0
  175. package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -0
  176. package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -0
  177. package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -0
  178. package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -0
  179. package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -0
  180. package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -0
  181. package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -0
  182. package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -0
  183. package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -0
  184. package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -0
  185. package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -0
  186. package/vendor/chatgpt2api/source/web/tsconfig.json +42 -0
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi.responses import JSONResponse
6
+
7
+
8
+ def _message_from_value(value: object) -> str:
9
+ if isinstance(value, str):
10
+ return value
11
+ if not isinstance(value, dict):
12
+ return ""
13
+ message = value.get("message")
14
+ if isinstance(message, str) and message:
15
+ return message
16
+ return _message_from_value(value.get("error"))
17
+
18
+
19
+ def error_message_from_detail(detail: object) -> str:
20
+ if isinstance(detail, list):
21
+ messages = []
22
+ for item in detail:
23
+ if not isinstance(item, dict):
24
+ continue
25
+ location = ".".join(str(part) for part in item.get("loc", []) if part != "body")
26
+ message = str(item.get("msg") or "").strip()
27
+ if location and message:
28
+ messages.append(f"{location}: {message}")
29
+ elif message:
30
+ messages.append(message)
31
+ return "; ".join(messages)
32
+ if isinstance(detail, dict):
33
+ message = _message_from_value(detail.get("error")) or _message_from_value(detail)
34
+ if message:
35
+ return message
36
+ return str(detail or "").strip()
37
+
38
+
39
+ def _default_error_type(status_code: int) -> str:
40
+ if status_code == 401:
41
+ return "authentication_error"
42
+ if status_code == 403:
43
+ return "permission_error"
44
+ if status_code == 429:
45
+ return "rate_limit_error"
46
+ if 400 <= status_code < 500:
47
+ return "invalid_request_error"
48
+ return "server_error"
49
+
50
+
51
+ def _default_error_code(status_code: int) -> str:
52
+ if status_code == 401:
53
+ return "invalid_api_key"
54
+ if status_code == 403:
55
+ return "permission_denied"
56
+ if status_code == 429:
57
+ return "rate_limit_exceeded"
58
+ if 400 <= status_code < 500:
59
+ return "bad_request"
60
+ return "upstream_error"
61
+
62
+
63
+ def openai_error_payload(
64
+ detail: object,
65
+ status_code: int,
66
+ *,
67
+ error_type: str | None = None,
68
+ code: object | None = None,
69
+ param: object | None = None,
70
+ ) -> dict[str, Any]:
71
+ error_detail = detail.get("error") if isinstance(detail, dict) else None
72
+ if isinstance(error_detail, dict):
73
+ return {
74
+ "error": {
75
+ "message": error_message_from_detail(error_detail) or "request failed",
76
+ "type": str(error_detail.get("type") or error_type or _default_error_type(status_code)),
77
+ "param": error_detail.get("param", param),
78
+ "code": error_detail.get("code", code if code is not None else _default_error_code(status_code)),
79
+ }
80
+ }
81
+ return {
82
+ "error": {
83
+ "message": error_message_from_detail(detail) or "request failed",
84
+ "type": error_type or _default_error_type(status_code),
85
+ "param": param,
86
+ "code": code if code is not None else _default_error_code(status_code),
87
+ }
88
+ }
89
+
90
+
91
+ def openai_error_response(
92
+ detail: object,
93
+ status_code: int,
94
+ *,
95
+ headers: dict[str, str] | None = None,
96
+ error_type: str | None = None,
97
+ code: object | None = None,
98
+ param: object | None = None,
99
+ ) -> JSONResponse:
100
+ return JSONResponse(
101
+ status_code=status_code,
102
+ content=openai_error_payload(detail, status_code, error_type=error_type, code=code, param=param),
103
+ headers=headers,
104
+ )
105
+
106
+
107
+ def anthropic_error_response(
108
+ detail: object,
109
+ status_code: int,
110
+ *,
111
+ headers: dict[str, str] | None = None,
112
+ ) -> JSONResponse:
113
+ error_type = "api_error" if status_code >= 500 else _default_error_type(status_code)
114
+ return JSONResponse(
115
+ status_code=status_code,
116
+ content={
117
+ "type": "error",
118
+ "error": {
119
+ "type": error_type,
120
+ "message": error_message_from_detail(detail) or "request failed",
121
+ },
122
+ },
123
+ headers=headers,
124
+ )
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from services.account_service import account_service
4
+ from services.openai_backend_api import OpenAIBackendAPI, SEARCH_MODEL
5
+
6
+ MODEL = SEARCH_MODEL
7
+
8
+
9
+ def handle(body: dict[str, object]) -> dict[str, object]:
10
+ token = account_service.get_text_access_token()
11
+ account = account_service.get_account(token) or {}
12
+ backend = OpenAIBackendAPI(token)
13
+ try:
14
+ result = backend.search(str(body["prompt"]))
15
+ finally:
16
+ backend.close()
17
+ account_service.mark_text_used(token)
18
+ result["_account_email"] = str(account.get("email") or "")
19
+ return result
@@ -0,0 +1,316 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from typing import Any, Iterable, Iterator
6
+
7
+ from fastapi import HTTPException
8
+
9
+ from services.protocol.chat_completion_cache import cache_key, chat_completion_cache, normalize_text_messages
10
+ from services.protocol.conversation import (
11
+ ConversationRequest,
12
+ ImageOutput,
13
+ collect_image_outputs,
14
+ collect_text,
15
+ count_message_image_tokens,
16
+ count_message_text_tokens,
17
+ count_text_tokens,
18
+ encode_images,
19
+ normalize_messages,
20
+ stream_image_outputs_with_pool,
21
+ stream_text_deltas,
22
+ text_backend,
23
+ )
24
+ from services.protocol.web_search_tool import (
25
+ WEB_SEARCH_TOOL_TYPES,
26
+ has_unsupported_tools,
27
+ is_web_search_chat_request,
28
+ run_web_search,
29
+ search_query_from_messages,
30
+ text_with_url_citations,
31
+ )
32
+ from utils.helper import build_chat_image_markdown_content, extract_chat_image, extract_chat_prompt, is_image_chat_request, parse_image_count
33
+ from utils.image_tokens import (
34
+ chat_usage_from_image_usage,
35
+ count_image_inputs_tokens,
36
+ count_image_output_items_tokens,
37
+ image_usage,
38
+ )
39
+
40
+ TOOL_UNAVAILABLE_SYSTEM_MESSAGE = (
41
+ "This compatibility backend cannot execute local tools, shell commands, non-search tools, "
42
+ "or file operations. Do not claim to have run tools or inspected external resources. "
43
+ "If a user asks you to use a tool, say that tool execution is unavailable through this backend."
44
+ )
45
+
46
+
47
+ def normalize_thinking_effort(value: object) -> str:
48
+ normalized = str(value or "").strip().lower()
49
+ if normalized in {"", "none"}:
50
+ return ""
51
+ if normalized in {"low", "medium", "high"}:
52
+ return normalized
53
+ if normalized in {"xhigh", "extended"}:
54
+ return "extended"
55
+ return ""
56
+
57
+
58
+ def thinking_effort_from_body(body: dict[str, Any]) -> str:
59
+ if "thinking_effort" in body:
60
+ return normalize_thinking_effort(body.get("thinking_effort"))
61
+ if "reasoning_effort" in body:
62
+ return normalize_thinking_effort(body.get("reasoning_effort"))
63
+ reasoning = body.get("reasoning")
64
+ if isinstance(reasoning, dict):
65
+ return normalize_thinking_effort(reasoning.get("effort"))
66
+ return ""
67
+
68
+
69
+ def completion_chunk(model: str, delta: dict[str, Any], finish_reason: str | None = None, completion_id: str = "", created: int | None = None) -> dict[str, Any]:
70
+ return {
71
+ "id": completion_id or f"chatcmpl-{uuid.uuid4().hex}",
72
+ "object": "chat.completion.chunk",
73
+ "created": created or int(time.time()),
74
+ "model": model,
75
+ "choices": [{"index": 0, "delta": delta, "finish_reason": finish_reason}],
76
+ }
77
+
78
+
79
+ def completion_response(
80
+ model: str,
81
+ content: str,
82
+ created: int | None = None,
83
+ messages: list[dict[str, Any]] | None = None,
84
+ annotations: list[dict[str, Any]] | None = None,
85
+ ) -> dict[str, Any]:
86
+ prompt_text_tokens = count_message_text_tokens(messages, model) if messages else 0
87
+ prompt_image_tokens = count_message_image_tokens(messages, model) if messages else 0
88
+ prompt_tokens = prompt_text_tokens + prompt_image_tokens
89
+ completion_tokens = count_text_tokens(content, model) if messages else 0
90
+ message = {"role": "assistant", "content": content}
91
+ if annotations:
92
+ message["annotations"] = annotations
93
+ return {
94
+ "id": f"chatcmpl-{uuid.uuid4().hex}",
95
+ "object": "chat.completion",
96
+ "created": created or int(time.time()),
97
+ "model": model,
98
+ "choices": [{
99
+ "index": 0,
100
+ "message": message,
101
+ "finish_reason": "stop",
102
+ }],
103
+ "usage": {
104
+ "prompt_tokens": prompt_tokens,
105
+ "completion_tokens": completion_tokens,
106
+ "total_tokens": prompt_tokens + completion_tokens,
107
+ "prompt_tokens_details": {
108
+ "text_tokens": prompt_text_tokens,
109
+ "image_tokens": prompt_image_tokens,
110
+ "cached_tokens": 0,
111
+ },
112
+ "completion_tokens_details": {
113
+ "text_tokens": completion_tokens,
114
+ "image_tokens": 0,
115
+ "reasoning_tokens": 0,
116
+ },
117
+ },
118
+ }
119
+
120
+
121
+ def stream_text_chat_completion(
122
+ backend,
123
+ messages: list[dict[str, Any]],
124
+ model: str,
125
+ thinking_effort: str = "",
126
+ ) -> Iterator[dict[str, Any]]:
127
+ completion_id = f"chatcmpl-{uuid.uuid4().hex}"
128
+ created = int(time.time())
129
+ sent_role = False
130
+ request = ConversationRequest(model=model, messages=messages, thinking_effort=thinking_effort)
131
+ for delta_text in stream_text_deltas(backend, request):
132
+ if not sent_role:
133
+ sent_role = True
134
+ yield completion_chunk(model, {"role": "assistant", "content": delta_text}, None, completion_id, created)
135
+ else:
136
+ yield completion_chunk(model, {"content": delta_text}, None, completion_id, created)
137
+ if not sent_role:
138
+ yield completion_chunk(model, {"role": "assistant", "content": ""}, None, completion_id, created)
139
+ yield completion_chunk(model, {}, "stop", completion_id, created)
140
+
141
+
142
+ def collect_chat_content(chunks: Iterable[dict[str, Any]]) -> str:
143
+ parts: list[str] = []
144
+ for chunk in chunks:
145
+ choices = chunk.get("choices")
146
+ first = choices[0] if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
147
+ delta = first.get("delta") if isinstance(first.get("delta"), dict) else {}
148
+ content = str(delta.get("content") or "")
149
+ if content:
150
+ parts.append(content)
151
+ return "".join(parts)
152
+
153
+
154
+ def chat_messages_from_body(body: dict[str, Any]) -> list[dict[str, Any]]:
155
+ messages = body.get("messages")
156
+ if isinstance(messages, list) and messages:
157
+ return [message for message in messages if isinstance(message, dict)]
158
+ prompt = str(body.get("prompt") or "").strip()
159
+ if prompt:
160
+ return [{"role": "user", "content": prompt}]
161
+ raise HTTPException(status_code=400, detail={"error": "messages or prompt is required"})
162
+
163
+
164
+ def chat_image_args(body: dict[str, Any]) -> tuple[str, str, int, list[tuple[bytes, str, str]]]:
165
+ model = str(body.get("model") or "gpt-image-2").strip() or "gpt-image-2"
166
+ prompt = extract_chat_prompt(body)
167
+ if not prompt:
168
+ raise HTTPException(status_code=400, detail={"error": "prompt is required"})
169
+ images = [
170
+ (data, f"image_{idx}.png", mime)
171
+ for idx, (data, mime) in enumerate(extract_chat_image(body), start=1)
172
+ ]
173
+ return model, prompt, parse_image_count(body.get("n")), images
174
+
175
+
176
+ def text_chat_parts(body: dict[str, Any]) -> tuple[str, list[dict[str, Any]]]:
177
+ model = str(body.get("model") or "auto").strip() or "auto"
178
+ messages = normalize_text_messages(normalize_messages(chat_messages_from_body(body)))
179
+ if has_unsupported_tools(body, WEB_SEARCH_TOOL_TYPES):
180
+ messages.insert(0, {"role": "system", "content": TOOL_UNAVAILABLE_SYSTEM_MESSAGE})
181
+ return model, messages
182
+
183
+
184
+ def chat_completion_annotations(annotations: list[dict[str, Any]]) -> list[dict[str, Any]]:
185
+ output = []
186
+ for item in annotations:
187
+ if item.get("type") != "url_citation":
188
+ continue
189
+ output.append({
190
+ "type": "url_citation",
191
+ "url_citation": {
192
+ "start_index": item.get("start_index", 0),
193
+ "end_index": item.get("end_index", 0),
194
+ "url": item.get("url", ""),
195
+ "title": item.get("title", ""),
196
+ },
197
+ })
198
+ return output
199
+
200
+
201
+ def web_search_chat_response(messages: list[dict[str, Any]], model: str) -> dict[str, Any]:
202
+ query = search_query_from_messages(messages)
203
+ if not query:
204
+ raise HTTPException(status_code=400, detail={"error": "messages or prompt is required for web search"})
205
+ text, annotations = text_with_url_citations(run_web_search(query))
206
+ return completion_response(
207
+ model,
208
+ text,
209
+ messages=messages,
210
+ annotations=chat_completion_annotations(annotations),
211
+ )
212
+
213
+
214
+ def stream_web_search_chat_completion(messages: list[dict[str, Any]], model: str) -> Iterator[dict[str, Any]]:
215
+ query = search_query_from_messages(messages)
216
+ if not query:
217
+ raise HTTPException(status_code=400, detail={"error": "messages or prompt is required for web search"})
218
+ text, _annotations = text_with_url_citations(run_web_search(query))
219
+ completion_id = f"chatcmpl-{uuid.uuid4().hex}"
220
+ created = int(time.time())
221
+ yield completion_chunk(model, {"role": "assistant", "content": text}, None, completion_id, created)
222
+ yield completion_chunk(model, {}, "stop", completion_id, created)
223
+
224
+
225
+ def image_result_content(result: dict[str, Any]) -> str:
226
+ data = result.get("data")
227
+ if isinstance(data, list) and data:
228
+ return build_chat_image_markdown_content(result)
229
+ return str(result.get("message") or "Image generation completed.")
230
+
231
+
232
+ def image_chat_response(body: dict[str, Any]) -> dict[str, Any]:
233
+ model, prompt, n, images = chat_image_args(body)
234
+ result = collect_image_outputs(stream_image_outputs_with_pool(ConversationRequest(
235
+ prompt=prompt,
236
+ model=model,
237
+ n=n,
238
+ response_format="b64_json",
239
+ images=encode_images(images) or None,
240
+ )))
241
+ response = completion_response(model, image_result_content(result), int(result.get("created") or 0) or None)
242
+ usage = image_usage(
243
+ input_text_tokens=count_text_tokens(prompt, model),
244
+ input_image_tokens=count_image_inputs_tokens(images, model),
245
+ output_tokens=count_image_output_items_tokens(result.get("data")),
246
+ )
247
+ response["usage"] = chat_usage_from_image_usage(usage)
248
+ return response
249
+
250
+
251
+ def image_chat_events(body: dict[str, Any]) -> Iterator[dict[str, Any]]:
252
+ model, prompt, n, images = chat_image_args(body)
253
+ image_outputs = stream_image_outputs_with_pool(ConversationRequest(
254
+ prompt=prompt,
255
+ model=model,
256
+ n=n,
257
+ response_format="b64_json",
258
+ images=encode_images(images) or None,
259
+ ))
260
+ yield from stream_image_chat_completion(image_outputs, model)
261
+
262
+
263
+ def stream_image_chat_completion(image_outputs: Iterable[ImageOutput], model: str) -> Iterator[dict[str, Any]]:
264
+ completion_id = f"chatcmpl-{uuid.uuid4().hex}"
265
+ created = int(time.time())
266
+ sent_role = False
267
+ sent_text = ""
268
+ for output in image_outputs:
269
+ content = ""
270
+ if output.kind == "progress":
271
+ content = output.text
272
+ sent_text += content
273
+ elif output.kind == "result":
274
+ content = build_chat_image_markdown_content({"data": output.data})
275
+ elif output.kind == "message":
276
+ content = output.text[len(sent_text):] if output.text.startswith(sent_text) else output.text
277
+ if not content:
278
+ continue
279
+ if not sent_role:
280
+ sent_role = True
281
+ yield completion_chunk(model, {"role": "assistant", "content": content}, None, completion_id, created)
282
+ else:
283
+ yield completion_chunk(model, {"content": content}, None, completion_id, created)
284
+ if not sent_role:
285
+ yield completion_chunk(model, {"role": "assistant", "content": ""}, None, completion_id, created)
286
+ yield completion_chunk(model, {}, "stop", completion_id, created)
287
+
288
+
289
+ def handle(body: dict[str, Any]) -> dict[str, Any] | Iterator[dict[str, Any]]:
290
+ if body.get("stream"):
291
+ if is_image_chat_request(body):
292
+ return image_chat_events(body)
293
+ model, messages = text_chat_parts(body)
294
+ if is_web_search_chat_request(body) and not has_unsupported_tools(body, WEB_SEARCH_TOOL_TYPES):
295
+ return stream_web_search_chat_completion(messages, model)
296
+ thinking_effort = thinking_effort_from_body(body)
297
+ key = cache_key(body, messages, stream=True)
298
+ return chat_completion_cache.get_or_compute_stream(
299
+ key,
300
+ lambda: stream_text_chat_completion(text_backend(model), messages, model, thinking_effort),
301
+ )
302
+ if is_image_chat_request(body):
303
+ return image_chat_response(body)
304
+ model, messages = text_chat_parts(body)
305
+ if is_web_search_chat_request(body) and not has_unsupported_tools(body, WEB_SEARCH_TOOL_TYPES):
306
+ return web_search_chat_response(messages, model)
307
+ thinking_effort = thinking_effort_from_body(body)
308
+ key = cache_key(body, messages, stream=False)
309
+ return chat_completion_cache.get_or_compute_response(
310
+ key,
311
+ lambda: completion_response(
312
+ model,
313
+ collect_text(text_backend(model), ConversationRequest(model=model, messages=messages, thinking_effort=thinking_effort)),
314
+ messages=messages,
315
+ ),
316
+ )
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from io import BytesIO
4
+ from typing import Any, Iterator
5
+
6
+ from PIL import Image
7
+
8
+ from services.protocol.conversation import (
9
+ ConversationRequest,
10
+ ImageGenerationError,
11
+ collect_image_outputs,
12
+ count_text_tokens,
13
+ encode_images,
14
+ stream_image_chunks,
15
+ stream_image_outputs_with_pool,
16
+ )
17
+ from utils.image_tokens import count_image_inputs_tokens, count_image_output_items_tokens, image_usage
18
+
19
+
20
+ def _composite_mask(
21
+ images: list[tuple[bytes, str, str]],
22
+ masks: list[tuple[bytes, str, str]],
23
+ ) -> list[tuple[bytes, str, str]]:
24
+ """将 mask 的 alpha 通道合成到图片中,标识需要编辑的区域。
25
+
26
+ mask 的透明区域(低 alpha)= 需要编辑的区域,
27
+ mask 的不透明区域(高 alpha)= 保留的区域。
28
+ 如果无 mask 则返回原图。
29
+ """
30
+ if not masks:
31
+ return images
32
+ result: list[tuple[bytes, str, str]] = []
33
+ for i, (data, filename, mime_type) in enumerate(images):
34
+ mask_data = masks[i][0] if i < len(masks) else masks[-1][0]
35
+ img = Image.open(BytesIO(data)).convert("RGBA")
36
+ mask_img = Image.open(BytesIO(mask_data))
37
+ if mask_img.mode == "RGBA":
38
+ alpha = mask_img.split()[3]
39
+ elif mask_img.mode == "L":
40
+ alpha = mask_img
41
+ else:
42
+ alpha = mask_img.convert("L")
43
+ alpha = alpha.resize(img.size, Image.LANCZOS)
44
+ img.putalpha(alpha)
45
+ buf = BytesIO()
46
+ img.save(buf, format="PNG")
47
+ result.append((buf.getvalue(), filename, "image/png"))
48
+ return result
49
+
50
+
51
+ def handle(body: dict[str, Any]) -> dict[str, Any] | Iterator[dict[str, Any]]:
52
+ prompt = str(body.get("prompt") or "")
53
+ images = body.get("images") or []
54
+ masks = body.get("mask") or []
55
+ images = _composite_mask(images, masks)
56
+ model = str(body.get("model") or "gpt-image-2")
57
+ n = int(body.get("n") or 1)
58
+ size = body.get("size")
59
+ quality = str(body.get("quality") or "auto")
60
+ response_format = str(body.get("response_format") or "b64_json")
61
+ base_url = str(body.get("base_url") or "") or None
62
+ progress_callback = body.get("progress_callback")
63
+ encoded_images = encode_images(images)
64
+ if not encoded_images:
65
+ raise ImageGenerationError("image is required")
66
+ outputs = stream_image_outputs_with_pool(ConversationRequest(
67
+ prompt=prompt,
68
+ model=model,
69
+ n=n,
70
+ size=size,
71
+ quality=quality,
72
+ response_format=response_format,
73
+ base_url=base_url,
74
+ images=encoded_images,
75
+ message_as_error=True,
76
+ progress_callback=progress_callback,
77
+ ))
78
+ if body.get("stream"):
79
+ return stream_image_chunks(outputs)
80
+ result = collect_image_outputs(outputs)
81
+ result["usage"] = image_usage(
82
+ input_text_tokens=count_text_tokens(prompt, model),
83
+ input_image_tokens=count_image_inputs_tokens(images, model),
84
+ output_tokens=count_image_output_items_tokens(result.get("data"), size, quality),
85
+ )
86
+ return result
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterator
4
+
5
+ from services.protocol.conversation import (
6
+ ConversationRequest,
7
+ collect_image_outputs,
8
+ count_text_tokens,
9
+ stream_image_chunks,
10
+ stream_image_outputs_with_pool,
11
+ )
12
+ from utils.image_tokens import count_image_output_items_tokens, image_usage
13
+
14
+
15
+ def handle(body: dict[str, Any]) -> dict[str, Any] | Iterator[dict[str, Any]]:
16
+ prompt = str(body.get("prompt") or "")
17
+ model = str(body.get("model") or "gpt-image-2")
18
+ n = int(body.get("n") or 1)
19
+ size = body.get("size")
20
+ quality = str(body.get("quality") or "auto")
21
+ response_format = str(body.get("response_format") or "b64_json")
22
+ base_url = str(body.get("base_url") or "") or None
23
+ progress_callback = body.get("progress_callback")
24
+ outputs = stream_image_outputs_with_pool(ConversationRequest(
25
+ prompt=prompt,
26
+ model=model,
27
+ n=n,
28
+ size=size,
29
+ quality=quality,
30
+ response_format=response_format,
31
+ base_url=base_url,
32
+ message_as_error=True,
33
+ progress_callback=progress_callback,
34
+ ))
35
+ if body.get("stream"):
36
+ return stream_image_chunks(outputs)
37
+ result = collect_image_outputs(outputs)
38
+ result["usage"] = image_usage(
39
+ input_text_tokens=count_text_tokens(prompt, model),
40
+ output_tokens=count_image_output_items_tokens(result.get("data"), size, quality),
41
+ )
42
+ return result
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from services.account_service import account_service
6
+ from services.model_service import model_catalog_service
7
+ from utils.helper import CODEX_IMAGE_MODEL
8
+
9
+
10
+ def list_models() -> dict[str, Any]:
11
+ result = model_catalog_service.list_models()
12
+ data = result.get("data")
13
+ if not isinstance(data, list):
14
+ return result
15
+ seen = {str(item.get("id") or "").strip() for item in data if isinstance(item, dict)}
16
+ dynamic_models: set[str] = set()
17
+ accounts = account_service.list_accounts()
18
+ web_image_accounts = [
19
+ account
20
+ for account in accounts
21
+ if isinstance(account, dict)
22
+ ]
23
+ codex_types = {
24
+ normalized
25
+ for account in accounts
26
+ if isinstance(account, dict)
27
+ and account_service._normalize_source_type(account.get("source_type")) == "codex"
28
+ and (normalized := account_service._normalize_account_type(account.get("type")))
29
+ }
30
+
31
+ if web_image_accounts:
32
+ dynamic_models.add("gpt-image-2")
33
+ if codex_types & {"Plus", "Team", "Pro"}:
34
+ dynamic_models.add(CODEX_IMAGE_MODEL)
35
+ if "Plus" in codex_types:
36
+ dynamic_models.add(f"plus-{CODEX_IMAGE_MODEL}")
37
+ if "Team" in codex_types:
38
+ dynamic_models.add(f"team-{CODEX_IMAGE_MODEL}")
39
+ if "Pro" in codex_types:
40
+ dynamic_models.add(f"pro-{CODEX_IMAGE_MODEL}")
41
+
42
+ for model in sorted(dynamic_models):
43
+ if model not in seen:
44
+ data.append({
45
+ "id": model,
46
+ "object": "model",
47
+ "created": 0,
48
+ "owned_by": "chatgpt2api",
49
+ "permission": [],
50
+ "root": model,
51
+ "parent": None,
52
+ })
53
+ return result