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,182 +1,182 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from collections.abc import Callable
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
from threading import RLock
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from services.account_service import AccountService, account_service
|
|
11
|
-
from services.openai_backend_api import OpenAIBackendAPI
|
|
12
|
-
from utils.log import logger
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@dataclass(frozen=True)
|
|
16
|
-
class ModelRoute:
|
|
17
|
-
account_types: frozenset[str]
|
|
18
|
-
allow_anonymous: bool = False
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ModelUnavailableError(RuntimeError):
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class ModelCatalogService:
|
|
26
|
-
"""Caches the model catalogs advertised to each active account type."""
|
|
27
|
-
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
accounts: AccountService,
|
|
31
|
-
*,
|
|
32
|
-
backend_factory: Callable[..., Any] = OpenAIBackendAPI,
|
|
33
|
-
cache_ttl_seconds: float = 300,
|
|
34
|
-
clock: Callable[[], float] = time.monotonic,
|
|
35
|
-
) -> None:
|
|
36
|
-
self._accounts = accounts
|
|
37
|
-
self._backend_factory = backend_factory
|
|
38
|
-
self._cache_ttl_seconds = max(1.0, float(cache_ttl_seconds))
|
|
39
|
-
self._clock = clock
|
|
40
|
-
self._lock = RLock()
|
|
41
|
-
self._expires_at = 0.0
|
|
42
|
-
self._account_signature: tuple[tuple[str, int], ...] = ()
|
|
43
|
-
self._anonymous_models: dict[str, dict[str, Any]] = {}
|
|
44
|
-
self._models_by_account_type: dict[str, dict[str, dict[str, Any]]] = {}
|
|
45
|
-
|
|
46
|
-
@staticmethod
|
|
47
|
-
def _model_map(result: object) -> dict[str, dict[str, Any]]:
|
|
48
|
-
if not isinstance(result, dict) or not isinstance(result.get("data"), list):
|
|
49
|
-
raise TypeError("upstream model response has no data list")
|
|
50
|
-
models: dict[str, dict[str, Any]] = {}
|
|
51
|
-
for item in result["data"]:
|
|
52
|
-
if not isinstance(item, dict):
|
|
53
|
-
continue
|
|
54
|
-
model_id = str(item.get("id") or "").strip()
|
|
55
|
-
if model_id and model_id not in models:
|
|
56
|
-
models[model_id] = dict(item)
|
|
57
|
-
return models
|
|
58
|
-
|
|
59
|
-
def _active_accounts_by_type(self) -> dict[str, list[str]]:
|
|
60
|
-
groups: dict[str, list[str]] = {}
|
|
61
|
-
for account in self._accounts.list_accounts():
|
|
62
|
-
if not isinstance(account, dict) or account.get("status") in {"禁用", "异常"}:
|
|
63
|
-
continue
|
|
64
|
-
access_token = str(account.get("access_token") or "").strip()
|
|
65
|
-
account_type = self._accounts._normalize_account_type(account.get("type"))
|
|
66
|
-
if access_token and account_type:
|
|
67
|
-
groups.setdefault(account_type, []).append(access_token)
|
|
68
|
-
return groups
|
|
69
|
-
|
|
70
|
-
@staticmethod
|
|
71
|
-
def _signature(groups: dict[str, list[str]]) -> tuple[tuple[str, int], ...]:
|
|
72
|
-
return tuple(
|
|
73
|
-
(account_type, len(tokens))
|
|
74
|
-
for account_type, tokens in sorted(groups.items())
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
def _fetch_models(self, access_token: str = "") -> dict[str, dict[str, Any]]:
|
|
78
|
-
backend = self._backend_factory(access_token=access_token)
|
|
79
|
-
try:
|
|
80
|
-
return self._model_map(backend.list_models())
|
|
81
|
-
finally:
|
|
82
|
-
backend.close()
|
|
83
|
-
|
|
84
|
-
def _fetch_account_type_models(
|
|
85
|
-
self,
|
|
86
|
-
account_type: str,
|
|
87
|
-
access_tokens: list[str],
|
|
88
|
-
) -> dict[str, dict[str, Any]] | None:
|
|
89
|
-
attempted_tokens: set[str] = set()
|
|
90
|
-
last_error: Exception | None = None
|
|
91
|
-
for access_token in access_tokens:
|
|
92
|
-
try:
|
|
93
|
-
resolved_token = self._accounts.refresh_access_token(
|
|
94
|
-
access_token,
|
|
95
|
-
event="list_models",
|
|
96
|
-
) or access_token
|
|
97
|
-
if resolved_token in attempted_tokens:
|
|
98
|
-
continue
|
|
99
|
-
attempted_tokens.add(resolved_token)
|
|
100
|
-
return self._fetch_models(resolved_token)
|
|
101
|
-
except Exception as exc: # noqa: BLE001 - try the next account for any upstream failure
|
|
102
|
-
last_error = exc
|
|
103
|
-
if last_error is not None:
|
|
104
|
-
logger.warning({
|
|
105
|
-
"event": "model_catalog_account_type_failed",
|
|
106
|
-
"account_type": account_type,
|
|
107
|
-
"error_type": type(last_error).__name__,
|
|
108
|
-
})
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
def _refresh(self, groups: dict[str, list[str]], signature: tuple[tuple[str, int], ...]) -> None:
|
|
112
|
-
models_by_account_type: dict[str, dict[str, dict[str, Any]]] = {}
|
|
113
|
-
with ThreadPoolExecutor(max_workers=min(4, len(groups) + 1)) as executor:
|
|
114
|
-
anonymous_future = executor.submit(self._fetch_models)
|
|
115
|
-
account_futures = {
|
|
116
|
-
account_type: executor.submit(
|
|
117
|
-
self._fetch_account_type_models,
|
|
118
|
-
account_type,
|
|
119
|
-
access_tokens,
|
|
120
|
-
)
|
|
121
|
-
for account_type, access_tokens in groups.items()
|
|
122
|
-
}
|
|
123
|
-
try:
|
|
124
|
-
anonymous_models = anonymous_future.result()
|
|
125
|
-
except Exception as exc: # noqa: BLE001 - retain cached models on upstream failure
|
|
126
|
-
logger.warning({
|
|
127
|
-
"event": "model_catalog_anonymous_failed",
|
|
128
|
-
"error_type": type(exc).__name__,
|
|
129
|
-
})
|
|
130
|
-
anonymous_models = self._anonymous_models
|
|
131
|
-
|
|
132
|
-
for account_type, future in account_futures.items():
|
|
133
|
-
models = future.result()
|
|
134
|
-
if models is not None:
|
|
135
|
-
models_by_account_type[account_type] = models
|
|
136
|
-
elif account_type in self._models_by_account_type:
|
|
137
|
-
models_by_account_type[account_type] = self._models_by_account_type[account_type]
|
|
138
|
-
|
|
139
|
-
self._anonymous_models = anonymous_models
|
|
140
|
-
self._models_by_account_type = models_by_account_type
|
|
141
|
-
self._account_signature = signature
|
|
142
|
-
self._expires_at = self._clock() + self._cache_ttl_seconds
|
|
143
|
-
|
|
144
|
-
def _ensure_catalog(self) -> None:
|
|
145
|
-
groups = self._active_accounts_by_type()
|
|
146
|
-
signature = self._signature(groups)
|
|
147
|
-
with self._lock:
|
|
148
|
-
if signature == self._account_signature and self._clock() < self._expires_at:
|
|
149
|
-
return
|
|
150
|
-
self._refresh(groups, signature)
|
|
151
|
-
|
|
152
|
-
def list_models(self) -> dict[str, Any]:
|
|
153
|
-
self._ensure_catalog()
|
|
154
|
-
with self._lock:
|
|
155
|
-
union: dict[str, dict[str, Any]] = {
|
|
156
|
-
model_id: dict(item)
|
|
157
|
-
for model_id, item in self._anonymous_models.items()
|
|
158
|
-
}
|
|
159
|
-
for account_type in sorted(self._models_by_account_type):
|
|
160
|
-
for model_id, item in self._models_by_account_type[account_type].items():
|
|
161
|
-
union.setdefault(model_id, dict(item))
|
|
162
|
-
return {
|
|
163
|
-
"object": "list",
|
|
164
|
-
"data": [union[model_id] for model_id in sorted(union)],
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
def route_for_model(self, model: str) -> ModelRoute:
|
|
168
|
-
model = str(model or "").strip()
|
|
169
|
-
self._ensure_catalog()
|
|
170
|
-
with self._lock:
|
|
171
|
-
account_types = frozenset(
|
|
172
|
-
account_type
|
|
173
|
-
for account_type, models in self._models_by_account_type.items()
|
|
174
|
-
if model in models
|
|
175
|
-
)
|
|
176
|
-
return ModelRoute(
|
|
177
|
-
account_types=account_types,
|
|
178
|
-
allow_anonymous=model in self._anonymous_models,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
model_catalog_service = ModelCatalogService(account_service)
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from threading import RLock
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from services.account_service import AccountService, account_service
|
|
11
|
+
from services.openai_backend_api import OpenAIBackendAPI
|
|
12
|
+
from utils.log import logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ModelRoute:
|
|
17
|
+
account_types: frozenset[str]
|
|
18
|
+
allow_anonymous: bool = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ModelUnavailableError(RuntimeError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModelCatalogService:
|
|
26
|
+
"""Caches the model catalogs advertised to each active account type."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
accounts: AccountService,
|
|
31
|
+
*,
|
|
32
|
+
backend_factory: Callable[..., Any] = OpenAIBackendAPI,
|
|
33
|
+
cache_ttl_seconds: float = 300,
|
|
34
|
+
clock: Callable[[], float] = time.monotonic,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._accounts = accounts
|
|
37
|
+
self._backend_factory = backend_factory
|
|
38
|
+
self._cache_ttl_seconds = max(1.0, float(cache_ttl_seconds))
|
|
39
|
+
self._clock = clock
|
|
40
|
+
self._lock = RLock()
|
|
41
|
+
self._expires_at = 0.0
|
|
42
|
+
self._account_signature: tuple[tuple[str, int], ...] = ()
|
|
43
|
+
self._anonymous_models: dict[str, dict[str, Any]] = {}
|
|
44
|
+
self._models_by_account_type: dict[str, dict[str, dict[str, Any]]] = {}
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _model_map(result: object) -> dict[str, dict[str, Any]]:
|
|
48
|
+
if not isinstance(result, dict) or not isinstance(result.get("data"), list):
|
|
49
|
+
raise TypeError("upstream model response has no data list")
|
|
50
|
+
models: dict[str, dict[str, Any]] = {}
|
|
51
|
+
for item in result["data"]:
|
|
52
|
+
if not isinstance(item, dict):
|
|
53
|
+
continue
|
|
54
|
+
model_id = str(item.get("id") or "").strip()
|
|
55
|
+
if model_id and model_id not in models:
|
|
56
|
+
models[model_id] = dict(item)
|
|
57
|
+
return models
|
|
58
|
+
|
|
59
|
+
def _active_accounts_by_type(self) -> dict[str, list[str]]:
|
|
60
|
+
groups: dict[str, list[str]] = {}
|
|
61
|
+
for account in self._accounts.list_accounts():
|
|
62
|
+
if not isinstance(account, dict) or account.get("status") in {"禁用", "异常"}:
|
|
63
|
+
continue
|
|
64
|
+
access_token = str(account.get("access_token") or "").strip()
|
|
65
|
+
account_type = self._accounts._normalize_account_type(account.get("type"))
|
|
66
|
+
if access_token and account_type:
|
|
67
|
+
groups.setdefault(account_type, []).append(access_token)
|
|
68
|
+
return groups
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _signature(groups: dict[str, list[str]]) -> tuple[tuple[str, int], ...]:
|
|
72
|
+
return tuple(
|
|
73
|
+
(account_type, len(tokens))
|
|
74
|
+
for account_type, tokens in sorted(groups.items())
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def _fetch_models(self, access_token: str = "") -> dict[str, dict[str, Any]]:
|
|
78
|
+
backend = self._backend_factory(access_token=access_token)
|
|
79
|
+
try:
|
|
80
|
+
return self._model_map(backend.list_models())
|
|
81
|
+
finally:
|
|
82
|
+
backend.close()
|
|
83
|
+
|
|
84
|
+
def _fetch_account_type_models(
|
|
85
|
+
self,
|
|
86
|
+
account_type: str,
|
|
87
|
+
access_tokens: list[str],
|
|
88
|
+
) -> dict[str, dict[str, Any]] | None:
|
|
89
|
+
attempted_tokens: set[str] = set()
|
|
90
|
+
last_error: Exception | None = None
|
|
91
|
+
for access_token in access_tokens:
|
|
92
|
+
try:
|
|
93
|
+
resolved_token = self._accounts.refresh_access_token(
|
|
94
|
+
access_token,
|
|
95
|
+
event="list_models",
|
|
96
|
+
) or access_token
|
|
97
|
+
if resolved_token in attempted_tokens:
|
|
98
|
+
continue
|
|
99
|
+
attempted_tokens.add(resolved_token)
|
|
100
|
+
return self._fetch_models(resolved_token)
|
|
101
|
+
except Exception as exc: # noqa: BLE001 - try the next account for any upstream failure
|
|
102
|
+
last_error = exc
|
|
103
|
+
if last_error is not None:
|
|
104
|
+
logger.warning({
|
|
105
|
+
"event": "model_catalog_account_type_failed",
|
|
106
|
+
"account_type": account_type,
|
|
107
|
+
"error_type": type(last_error).__name__,
|
|
108
|
+
})
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _refresh(self, groups: dict[str, list[str]], signature: tuple[tuple[str, int], ...]) -> None:
|
|
112
|
+
models_by_account_type: dict[str, dict[str, dict[str, Any]]] = {}
|
|
113
|
+
with ThreadPoolExecutor(max_workers=min(4, len(groups) + 1)) as executor:
|
|
114
|
+
anonymous_future = executor.submit(self._fetch_models)
|
|
115
|
+
account_futures = {
|
|
116
|
+
account_type: executor.submit(
|
|
117
|
+
self._fetch_account_type_models,
|
|
118
|
+
account_type,
|
|
119
|
+
access_tokens,
|
|
120
|
+
)
|
|
121
|
+
for account_type, access_tokens in groups.items()
|
|
122
|
+
}
|
|
123
|
+
try:
|
|
124
|
+
anonymous_models = anonymous_future.result()
|
|
125
|
+
except Exception as exc: # noqa: BLE001 - retain cached models on upstream failure
|
|
126
|
+
logger.warning({
|
|
127
|
+
"event": "model_catalog_anonymous_failed",
|
|
128
|
+
"error_type": type(exc).__name__,
|
|
129
|
+
})
|
|
130
|
+
anonymous_models = self._anonymous_models
|
|
131
|
+
|
|
132
|
+
for account_type, future in account_futures.items():
|
|
133
|
+
models = future.result()
|
|
134
|
+
if models is not None:
|
|
135
|
+
models_by_account_type[account_type] = models
|
|
136
|
+
elif account_type in self._models_by_account_type:
|
|
137
|
+
models_by_account_type[account_type] = self._models_by_account_type[account_type]
|
|
138
|
+
|
|
139
|
+
self._anonymous_models = anonymous_models
|
|
140
|
+
self._models_by_account_type = models_by_account_type
|
|
141
|
+
self._account_signature = signature
|
|
142
|
+
self._expires_at = self._clock() + self._cache_ttl_seconds
|
|
143
|
+
|
|
144
|
+
def _ensure_catalog(self) -> None:
|
|
145
|
+
groups = self._active_accounts_by_type()
|
|
146
|
+
signature = self._signature(groups)
|
|
147
|
+
with self._lock:
|
|
148
|
+
if signature == self._account_signature and self._clock() < self._expires_at:
|
|
149
|
+
return
|
|
150
|
+
self._refresh(groups, signature)
|
|
151
|
+
|
|
152
|
+
def list_models(self) -> dict[str, Any]:
|
|
153
|
+
self._ensure_catalog()
|
|
154
|
+
with self._lock:
|
|
155
|
+
union: dict[str, dict[str, Any]] = {
|
|
156
|
+
model_id: dict(item)
|
|
157
|
+
for model_id, item in self._anonymous_models.items()
|
|
158
|
+
}
|
|
159
|
+
for account_type in sorted(self._models_by_account_type):
|
|
160
|
+
for model_id, item in self._models_by_account_type[account_type].items():
|
|
161
|
+
union.setdefault(model_id, dict(item))
|
|
162
|
+
return {
|
|
163
|
+
"object": "list",
|
|
164
|
+
"data": [union[model_id] for model_id in sorted(union)],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def route_for_model(self, model: str) -> ModelRoute:
|
|
168
|
+
model = str(model or "").strip()
|
|
169
|
+
self._ensure_catalog()
|
|
170
|
+
with self._lock:
|
|
171
|
+
account_types = frozenset(
|
|
172
|
+
account_type
|
|
173
|
+
for account_type, models in self._models_by_account_type.items()
|
|
174
|
+
if model in models
|
|
175
|
+
)
|
|
176
|
+
return ModelRoute(
|
|
177
|
+
account_types=account_types,
|
|
178
|
+
allow_anonymous=model in self._anonymous_models,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
model_catalog_service = ModelCatalogService(account_service)
|