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,217 +1,217 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from fastapi import APIRouter, Header, HTTPException, Request
|
|
4
|
-
from fastapi.concurrency import run_in_threadpool
|
|
5
|
-
from fastapi.responses import FileResponse
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
-
|
|
8
|
-
from api.image_inputs import parse_image_edit_request, read_image_sources
|
|
9
|
-
from api.support import require_identity, resolve_image_base_url
|
|
10
|
-
from services.content_filter import check_request, request_shape, request_text
|
|
11
|
-
from services.editable_file_task_service import editable_file_task_service
|
|
12
|
-
from services.log_service import LoggedCall
|
|
13
|
-
from services.protocol import (
|
|
14
|
-
anthropic_v1_messages,
|
|
15
|
-
openai_v1_chat_complete,
|
|
16
|
-
openai_v1_image_edit,
|
|
17
|
-
openai_v1_image_generations,
|
|
18
|
-
openai_v1_models,
|
|
19
|
-
openai_v1_response,
|
|
20
|
-
openai_search,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ImageGenerationRequest(BaseModel):
|
|
25
|
-
prompt: str = Field(..., min_length=1)
|
|
26
|
-
model: str = "gpt-image-2"
|
|
27
|
-
n: int = Field(default=1, ge=1, le=4)
|
|
28
|
-
size: str | None = None
|
|
29
|
-
quality: str = "auto"
|
|
30
|
-
response_format: str = "b64_json"
|
|
31
|
-
history_disabled: bool = True
|
|
32
|
-
stream: bool | None = None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ChatCompletionRequest(BaseModel):
|
|
36
|
-
model_config = ConfigDict(extra="allow")
|
|
37
|
-
model: str | None = None
|
|
38
|
-
prompt: str | None = None
|
|
39
|
-
n: int | None = None
|
|
40
|
-
stream: bool | None = None
|
|
41
|
-
modalities: list[str] | None = None
|
|
42
|
-
messages: list[dict[str, object]] | None = None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class ResponseCreateRequest(BaseModel):
|
|
46
|
-
model_config = ConfigDict(extra="allow")
|
|
47
|
-
model: str | None = None
|
|
48
|
-
input: object | None = None
|
|
49
|
-
tools: list[dict[str, object]] | None = None
|
|
50
|
-
tool_choice: object | None = None
|
|
51
|
-
stream: bool | None = None
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class AnthropicMessageRequest(BaseModel):
|
|
55
|
-
model_config = ConfigDict(extra="allow")
|
|
56
|
-
model: str | None = None
|
|
57
|
-
messages: list[dict[str, object]] | None = None
|
|
58
|
-
system: object | None = None
|
|
59
|
-
stream: bool | None = None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class SearchRequest(BaseModel):
|
|
63
|
-
prompt: str = Field(..., min_length=1)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class EditableFileTaskRequest(BaseModel):
|
|
67
|
-
prompt: str = ""
|
|
68
|
-
base64_images: list[str] = Field(default_factory=list)
|
|
69
|
-
client_task_id: str | None = None
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
async def filter_or_log(call: LoggedCall, text: str) -> None:
|
|
73
|
-
try:
|
|
74
|
-
await run_in_threadpool(check_request, text)
|
|
75
|
-
except HTTPException as exc:
|
|
76
|
-
call.log("调用失败", status="failed", error=str(exc.detail))
|
|
77
|
-
raise
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def create_router() -> APIRouter:
|
|
81
|
-
router = APIRouter()
|
|
82
|
-
|
|
83
|
-
@router.get("/v1/models")
|
|
84
|
-
async def list_models(authorization: str | None = Header(default=None)):
|
|
85
|
-
require_identity(authorization)
|
|
86
|
-
try:
|
|
87
|
-
return await run_in_threadpool(openai_v1_models.list_models)
|
|
88
|
-
except Exception as exc:
|
|
89
|
-
raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc
|
|
90
|
-
|
|
91
|
-
@router.post("/v1/images/generations")
|
|
92
|
-
async def generate_images(
|
|
93
|
-
body: ImageGenerationRequest,
|
|
94
|
-
request: Request,
|
|
95
|
-
authorization: str | None = Header(default=None),
|
|
96
|
-
):
|
|
97
|
-
identity = require_identity(authorization)
|
|
98
|
-
payload = body.model_dump(mode="python")
|
|
99
|
-
payload["base_url"] = resolve_image_base_url(request)
|
|
100
|
-
call = LoggedCall(identity, "/v1/images/generations", body.model, "文生图", request_text=body.prompt)
|
|
101
|
-
await filter_or_log(call, body.prompt)
|
|
102
|
-
return await call.run(openai_v1_image_generations.handle, payload)
|
|
103
|
-
|
|
104
|
-
@router.post("/v1/images/edits")
|
|
105
|
-
async def edit_images(
|
|
106
|
-
request: Request,
|
|
107
|
-
authorization: str | None = Header(default=None),
|
|
108
|
-
):
|
|
109
|
-
identity = require_identity(authorization)
|
|
110
|
-
payload, image_sources, mask_sources = await parse_image_edit_request(request)
|
|
111
|
-
prompt = str(payload["prompt"])
|
|
112
|
-
model = str(payload["model"])
|
|
113
|
-
call = LoggedCall(identity, "/v1/images/edits", model, "图生图", request_text=prompt)
|
|
114
|
-
await filter_or_log(call, prompt)
|
|
115
|
-
payload["images"] = await read_image_sources(image_sources)
|
|
116
|
-
if mask_sources:
|
|
117
|
-
payload["mask"] = await read_image_sources(mask_sources)
|
|
118
|
-
payload["base_url"] = resolve_image_base_url(request)
|
|
119
|
-
return await call.run(openai_v1_image_edit.handle, payload)
|
|
120
|
-
|
|
121
|
-
@router.post("/v1/chat/completions")
|
|
122
|
-
async def create_chat_completion(body: ChatCompletionRequest, authorization: str | None = Header(default=None)):
|
|
123
|
-
identity = require_identity(authorization)
|
|
124
|
-
payload = body.model_dump(mode="python")
|
|
125
|
-
model = str(payload.get("model") or "auto")
|
|
126
|
-
request_preview = request_text(payload.get("prompt"), payload.get("messages"))
|
|
127
|
-
call = LoggedCall(
|
|
128
|
-
identity,
|
|
129
|
-
"/v1/chat/completions",
|
|
130
|
-
model,
|
|
131
|
-
"文本生成",
|
|
132
|
-
request_text=request_preview,
|
|
133
|
-
request_shape=request_shape(payload.get("messages")),
|
|
134
|
-
)
|
|
135
|
-
await filter_or_log(call, request_preview)
|
|
136
|
-
return await call.run(openai_v1_chat_complete.handle, payload)
|
|
137
|
-
|
|
138
|
-
@router.post("/v1/responses")
|
|
139
|
-
async def create_response(body: ResponseCreateRequest, authorization: str | None = Header(default=None)):
|
|
140
|
-
identity = require_identity(authorization)
|
|
141
|
-
payload = body.model_dump(mode="python")
|
|
142
|
-
model = str(payload.get("model") or "auto")
|
|
143
|
-
request_preview = request_text(payload.get("input"), payload.get("instructions"))
|
|
144
|
-
call = LoggedCall(
|
|
145
|
-
identity,
|
|
146
|
-
"/v1/responses",
|
|
147
|
-
model,
|
|
148
|
-
"Responses",
|
|
149
|
-
request_text=request_preview,
|
|
150
|
-
request_shape=request_shape(payload.get("input")),
|
|
151
|
-
)
|
|
152
|
-
await filter_or_log(call, request_preview)
|
|
153
|
-
return await call.run(openai_v1_response.handle, payload)
|
|
154
|
-
|
|
155
|
-
@router.post("/v1/messages")
|
|
156
|
-
async def create_message(
|
|
157
|
-
body: AnthropicMessageRequest,
|
|
158
|
-
authorization: str | None = Header(default=None),
|
|
159
|
-
x_api_key: str | None = Header(default=None, alias="x-api-key"),
|
|
160
|
-
anthropic_version: str | None = Header(default=None, alias="anthropic-version"),
|
|
161
|
-
):
|
|
162
|
-
identity = require_identity(authorization or (f"Bearer {x_api_key}" if x_api_key else None))
|
|
163
|
-
payload = body.model_dump(mode="python")
|
|
164
|
-
model = str(payload.get("model") or "auto")
|
|
165
|
-
request_preview = request_text(payload.get("system"), payload.get("messages"), payload.get("tools"))
|
|
166
|
-
call = LoggedCall(identity, "/v1/messages", model, "Messages", request_text=request_preview)
|
|
167
|
-
await filter_or_log(call, request_preview)
|
|
168
|
-
return await call.run(anthropic_v1_messages.handle, payload, sse="anthropic")
|
|
169
|
-
|
|
170
|
-
@router.post("/v1/search")
|
|
171
|
-
async def search(body: SearchRequest, authorization: str | None = Header(default=None)):
|
|
172
|
-
identity = require_identity(authorization)
|
|
173
|
-
call = LoggedCall(identity, "/v1/search", openai_search.MODEL, "搜索", request_text=body.prompt)
|
|
174
|
-
await filter_or_log(call, body.prompt)
|
|
175
|
-
return await call.run(openai_search.handle, body.model_dump(mode="python"))
|
|
176
|
-
|
|
177
|
-
@router.get("/v1/editable-file-tasks")
|
|
178
|
-
async def list_editable_file_tasks(ids: str = "", authorization: str | None = Header(default=None)):
|
|
179
|
-
identity = require_identity(authorization)
|
|
180
|
-
task_ids = [item.strip() for item in ids.split(",") if item.strip()]
|
|
181
|
-
return await run_in_threadpool(editable_file_task_service.list_tasks, identity, task_ids)
|
|
182
|
-
|
|
183
|
-
@router.get("/files/{file_path:path}")
|
|
184
|
-
async def download_editable_file(file_path: str):
|
|
185
|
-
try:
|
|
186
|
-
path = await run_in_threadpool(editable_file_task_service.public_file_path, file_path)
|
|
187
|
-
except Exception as exc:
|
|
188
|
-
raise HTTPException(status_code=404, detail={"error": "file not found"}) from exc
|
|
189
|
-
return FileResponse(path, filename=path.name)
|
|
190
|
-
|
|
191
|
-
@router.post("/v1/ppt/generations")
|
|
192
|
-
async def create_ppt_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)):
|
|
193
|
-
identity = require_identity(authorization)
|
|
194
|
-
await filter_or_log(LoggedCall(identity, "/v1/ppt/generations", "gpt-5-5-thinking", "PPT生成任务", request_text=body.prompt), body.prompt)
|
|
195
|
-
return await run_in_threadpool(
|
|
196
|
-
editable_file_task_service.submit_ppt,
|
|
197
|
-
identity,
|
|
198
|
-
client_task_id=body.client_task_id or "",
|
|
199
|
-
prompt=body.prompt,
|
|
200
|
-
base64_images=body.base64_images,
|
|
201
|
-
base_url=resolve_image_base_url(request),
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
@router.post("/v1/psd/generations")
|
|
205
|
-
async def create_psd_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)):
|
|
206
|
-
identity = require_identity(authorization)
|
|
207
|
-
await filter_or_log(LoggedCall(identity, "/v1/psd/generations", "gpt-5-5-thinking", "PSD生成任务", request_text=body.prompt), body.prompt)
|
|
208
|
-
return await run_in_threadpool(
|
|
209
|
-
editable_file_task_service.submit_psd,
|
|
210
|
-
identity,
|
|
211
|
-
client_task_id=body.client_task_id or "",
|
|
212
|
-
prompt=body.prompt,
|
|
213
|
-
base64_images=body.base64_images,
|
|
214
|
-
base_url=resolve_image_base_url(request),
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
return router
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Header, HTTPException, Request
|
|
4
|
+
from fastapi.concurrency import run_in_threadpool
|
|
5
|
+
from fastapi.responses import FileResponse
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from api.image_inputs import parse_image_edit_request, read_image_sources
|
|
9
|
+
from api.support import require_identity, resolve_image_base_url
|
|
10
|
+
from services.content_filter import check_request, request_shape, request_text
|
|
11
|
+
from services.editable_file_task_service import editable_file_task_service
|
|
12
|
+
from services.log_service import LoggedCall
|
|
13
|
+
from services.protocol import (
|
|
14
|
+
anthropic_v1_messages,
|
|
15
|
+
openai_v1_chat_complete,
|
|
16
|
+
openai_v1_image_edit,
|
|
17
|
+
openai_v1_image_generations,
|
|
18
|
+
openai_v1_models,
|
|
19
|
+
openai_v1_response,
|
|
20
|
+
openai_search,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ImageGenerationRequest(BaseModel):
|
|
25
|
+
prompt: str = Field(..., min_length=1)
|
|
26
|
+
model: str = "gpt-image-2"
|
|
27
|
+
n: int = Field(default=1, ge=1, le=4)
|
|
28
|
+
size: str | None = None
|
|
29
|
+
quality: str = "auto"
|
|
30
|
+
response_format: str = "b64_json"
|
|
31
|
+
history_disabled: bool = True
|
|
32
|
+
stream: bool | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ChatCompletionRequest(BaseModel):
|
|
36
|
+
model_config = ConfigDict(extra="allow")
|
|
37
|
+
model: str | None = None
|
|
38
|
+
prompt: str | None = None
|
|
39
|
+
n: int | None = None
|
|
40
|
+
stream: bool | None = None
|
|
41
|
+
modalities: list[str] | None = None
|
|
42
|
+
messages: list[dict[str, object]] | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ResponseCreateRequest(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra="allow")
|
|
47
|
+
model: str | None = None
|
|
48
|
+
input: object | None = None
|
|
49
|
+
tools: list[dict[str, object]] | None = None
|
|
50
|
+
tool_choice: object | None = None
|
|
51
|
+
stream: bool | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AnthropicMessageRequest(BaseModel):
|
|
55
|
+
model_config = ConfigDict(extra="allow")
|
|
56
|
+
model: str | None = None
|
|
57
|
+
messages: list[dict[str, object]] | None = None
|
|
58
|
+
system: object | None = None
|
|
59
|
+
stream: bool | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SearchRequest(BaseModel):
|
|
63
|
+
prompt: str = Field(..., min_length=1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EditableFileTaskRequest(BaseModel):
|
|
67
|
+
prompt: str = ""
|
|
68
|
+
base64_images: list[str] = Field(default_factory=list)
|
|
69
|
+
client_task_id: str | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def filter_or_log(call: LoggedCall, text: str) -> None:
|
|
73
|
+
try:
|
|
74
|
+
await run_in_threadpool(check_request, text)
|
|
75
|
+
except HTTPException as exc:
|
|
76
|
+
call.log("调用失败", status="failed", error=str(exc.detail))
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_router() -> APIRouter:
|
|
81
|
+
router = APIRouter()
|
|
82
|
+
|
|
83
|
+
@router.get("/v1/models")
|
|
84
|
+
async def list_models(authorization: str | None = Header(default=None)):
|
|
85
|
+
require_identity(authorization)
|
|
86
|
+
try:
|
|
87
|
+
return await run_in_threadpool(openai_v1_models.list_models)
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
raise HTTPException(status_code=502, detail={"error": str(exc)}) from exc
|
|
90
|
+
|
|
91
|
+
@router.post("/v1/images/generations")
|
|
92
|
+
async def generate_images(
|
|
93
|
+
body: ImageGenerationRequest,
|
|
94
|
+
request: Request,
|
|
95
|
+
authorization: str | None = Header(default=None),
|
|
96
|
+
):
|
|
97
|
+
identity = require_identity(authorization)
|
|
98
|
+
payload = body.model_dump(mode="python")
|
|
99
|
+
payload["base_url"] = resolve_image_base_url(request)
|
|
100
|
+
call = LoggedCall(identity, "/v1/images/generations", body.model, "文生图", request_text=body.prompt)
|
|
101
|
+
await filter_or_log(call, body.prompt)
|
|
102
|
+
return await call.run(openai_v1_image_generations.handle, payload)
|
|
103
|
+
|
|
104
|
+
@router.post("/v1/images/edits")
|
|
105
|
+
async def edit_images(
|
|
106
|
+
request: Request,
|
|
107
|
+
authorization: str | None = Header(default=None),
|
|
108
|
+
):
|
|
109
|
+
identity = require_identity(authorization)
|
|
110
|
+
payload, image_sources, mask_sources = await parse_image_edit_request(request)
|
|
111
|
+
prompt = str(payload["prompt"])
|
|
112
|
+
model = str(payload["model"])
|
|
113
|
+
call = LoggedCall(identity, "/v1/images/edits", model, "图生图", request_text=prompt)
|
|
114
|
+
await filter_or_log(call, prompt)
|
|
115
|
+
payload["images"] = await read_image_sources(image_sources)
|
|
116
|
+
if mask_sources:
|
|
117
|
+
payload["mask"] = await read_image_sources(mask_sources)
|
|
118
|
+
payload["base_url"] = resolve_image_base_url(request)
|
|
119
|
+
return await call.run(openai_v1_image_edit.handle, payload)
|
|
120
|
+
|
|
121
|
+
@router.post("/v1/chat/completions")
|
|
122
|
+
async def create_chat_completion(body: ChatCompletionRequest, authorization: str | None = Header(default=None)):
|
|
123
|
+
identity = require_identity(authorization)
|
|
124
|
+
payload = body.model_dump(mode="python")
|
|
125
|
+
model = str(payload.get("model") or "auto")
|
|
126
|
+
request_preview = request_text(payload.get("prompt"), payload.get("messages"))
|
|
127
|
+
call = LoggedCall(
|
|
128
|
+
identity,
|
|
129
|
+
"/v1/chat/completions",
|
|
130
|
+
model,
|
|
131
|
+
"文本生成",
|
|
132
|
+
request_text=request_preview,
|
|
133
|
+
request_shape=request_shape(payload.get("messages")),
|
|
134
|
+
)
|
|
135
|
+
await filter_or_log(call, request_preview)
|
|
136
|
+
return await call.run(openai_v1_chat_complete.handle, payload)
|
|
137
|
+
|
|
138
|
+
@router.post("/v1/responses")
|
|
139
|
+
async def create_response(body: ResponseCreateRequest, authorization: str | None = Header(default=None)):
|
|
140
|
+
identity = require_identity(authorization)
|
|
141
|
+
payload = body.model_dump(mode="python")
|
|
142
|
+
model = str(payload.get("model") or "auto")
|
|
143
|
+
request_preview = request_text(payload.get("input"), payload.get("instructions"))
|
|
144
|
+
call = LoggedCall(
|
|
145
|
+
identity,
|
|
146
|
+
"/v1/responses",
|
|
147
|
+
model,
|
|
148
|
+
"Responses",
|
|
149
|
+
request_text=request_preview,
|
|
150
|
+
request_shape=request_shape(payload.get("input")),
|
|
151
|
+
)
|
|
152
|
+
await filter_or_log(call, request_preview)
|
|
153
|
+
return await call.run(openai_v1_response.handle, payload)
|
|
154
|
+
|
|
155
|
+
@router.post("/v1/messages")
|
|
156
|
+
async def create_message(
|
|
157
|
+
body: AnthropicMessageRequest,
|
|
158
|
+
authorization: str | None = Header(default=None),
|
|
159
|
+
x_api_key: str | None = Header(default=None, alias="x-api-key"),
|
|
160
|
+
anthropic_version: str | None = Header(default=None, alias="anthropic-version"),
|
|
161
|
+
):
|
|
162
|
+
identity = require_identity(authorization or (f"Bearer {x_api_key}" if x_api_key else None))
|
|
163
|
+
payload = body.model_dump(mode="python")
|
|
164
|
+
model = str(payload.get("model") or "auto")
|
|
165
|
+
request_preview = request_text(payload.get("system"), payload.get("messages"), payload.get("tools"))
|
|
166
|
+
call = LoggedCall(identity, "/v1/messages", model, "Messages", request_text=request_preview)
|
|
167
|
+
await filter_or_log(call, request_preview)
|
|
168
|
+
return await call.run(anthropic_v1_messages.handle, payload, sse="anthropic")
|
|
169
|
+
|
|
170
|
+
@router.post("/v1/search")
|
|
171
|
+
async def search(body: SearchRequest, authorization: str | None = Header(default=None)):
|
|
172
|
+
identity = require_identity(authorization)
|
|
173
|
+
call = LoggedCall(identity, "/v1/search", openai_search.MODEL, "搜索", request_text=body.prompt)
|
|
174
|
+
await filter_or_log(call, body.prompt)
|
|
175
|
+
return await call.run(openai_search.handle, body.model_dump(mode="python"))
|
|
176
|
+
|
|
177
|
+
@router.get("/v1/editable-file-tasks")
|
|
178
|
+
async def list_editable_file_tasks(ids: str = "", authorization: str | None = Header(default=None)):
|
|
179
|
+
identity = require_identity(authorization)
|
|
180
|
+
task_ids = [item.strip() for item in ids.split(",") if item.strip()]
|
|
181
|
+
return await run_in_threadpool(editable_file_task_service.list_tasks, identity, task_ids)
|
|
182
|
+
|
|
183
|
+
@router.get("/files/{file_path:path}")
|
|
184
|
+
async def download_editable_file(file_path: str):
|
|
185
|
+
try:
|
|
186
|
+
path = await run_in_threadpool(editable_file_task_service.public_file_path, file_path)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
raise HTTPException(status_code=404, detail={"error": "file not found"}) from exc
|
|
189
|
+
return FileResponse(path, filename=path.name)
|
|
190
|
+
|
|
191
|
+
@router.post("/v1/ppt/generations")
|
|
192
|
+
async def create_ppt_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)):
|
|
193
|
+
identity = require_identity(authorization)
|
|
194
|
+
await filter_or_log(LoggedCall(identity, "/v1/ppt/generations", "gpt-5-5-thinking", "PPT生成任务", request_text=body.prompt), body.prompt)
|
|
195
|
+
return await run_in_threadpool(
|
|
196
|
+
editable_file_task_service.submit_ppt,
|
|
197
|
+
identity,
|
|
198
|
+
client_task_id=body.client_task_id or "",
|
|
199
|
+
prompt=body.prompt,
|
|
200
|
+
base64_images=body.base64_images,
|
|
201
|
+
base_url=resolve_image_base_url(request),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@router.post("/v1/psd/generations")
|
|
205
|
+
async def create_psd_task(body: EditableFileTaskRequest, request: Request, authorization: str | None = Header(default=None)):
|
|
206
|
+
identity = require_identity(authorization)
|
|
207
|
+
await filter_or_log(LoggedCall(identity, "/v1/psd/generations", "gpt-5-5-thinking", "PSD生成任务", request_text=body.prompt), body.prompt)
|
|
208
|
+
return await run_in_threadpool(
|
|
209
|
+
editable_file_task_service.submit_psd,
|
|
210
|
+
identity,
|
|
211
|
+
client_task_id=body.client_task_id or "",
|
|
212
|
+
prompt=body.prompt,
|
|
213
|
+
base64_images=body.base64_images,
|
|
214
|
+
base_url=resolve_image_base_url(request),
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return router
|
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from contextlib import asynccontextmanager
|
|
4
|
-
from threading import Event
|
|
5
|
-
|
|
6
|
-
from fastapi import FastAPI, HTTPException
|
|
7
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
-
from fastapi.responses import FileResponse
|
|
9
|
-
|
|
10
|
-
from api import accounts, ai, image_tasks, system
|
|
11
|
-
from api.errors import install_exception_handlers
|
|
12
|
-
from api.support import resolve_web_asset, start_limited_account_watcher
|
|
13
|
-
from services.backup_service import backup_service
|
|
14
|
-
from services.config import config
|
|
15
|
-
from services.image_service import start_image_cleanup_scheduler
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def create_app() -> FastAPI:
|
|
19
|
-
app_version = config.app_version
|
|
20
|
-
|
|
21
|
-
@asynccontextmanager
|
|
22
|
-
async def lifespan(_: FastAPI):
|
|
23
|
-
stop_event = Event()
|
|
24
|
-
thread = start_limited_account_watcher(stop_event)
|
|
25
|
-
cleanup_thread = start_image_cleanup_scheduler(stop_event)
|
|
26
|
-
backup_service.start()
|
|
27
|
-
config.cleanup_old_images()
|
|
28
|
-
try:
|
|
29
|
-
yield
|
|
30
|
-
finally:
|
|
31
|
-
stop_event.set()
|
|
32
|
-
thread.join(timeout=1)
|
|
33
|
-
cleanup_thread.join(timeout=1)
|
|
34
|
-
backup_service.stop()
|
|
35
|
-
|
|
36
|
-
app = FastAPI(title="chatgpt2api", version=app_version, lifespan=lifespan)
|
|
37
|
-
install_exception_handlers(app)
|
|
38
|
-
app.add_middleware(
|
|
39
|
-
CORSMiddleware,
|
|
40
|
-
allow_origins=["*"],
|
|
41
|
-
allow_credentials=False,
|
|
42
|
-
allow_methods=["*"],
|
|
43
|
-
allow_headers=["*"],
|
|
44
|
-
)
|
|
45
|
-
app.include_router(ai.create_router())
|
|
46
|
-
app.include_router(accounts.create_router())
|
|
47
|
-
app.include_router(image_tasks.create_router())
|
|
48
|
-
app.include_router(system.create_router(app_version))
|
|
49
|
-
|
|
50
|
-
@app.api_route("/{full_path:path}", methods=["GET", "HEAD"], include_in_schema=False)
|
|
51
|
-
async def serve_web(full_path: str):
|
|
52
|
-
asset = resolve_web_asset(full_path)
|
|
53
|
-
if asset is not None:
|
|
54
|
-
return FileResponse(asset)
|
|
55
|
-
if full_path.strip("/").startswith("_next/"):
|
|
56
|
-
raise HTTPException(status_code=404, detail="Not Found")
|
|
57
|
-
fallback = resolve_web_asset("")
|
|
58
|
-
if fallback is None:
|
|
59
|
-
raise HTTPException(status_code=404, detail="Not Found")
|
|
60
|
-
return FileResponse(fallback)
|
|
61
|
-
|
|
62
|
-
return app
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from threading import Event
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, HTTPException
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
|
|
10
|
+
from api import accounts, ai, image_tasks, system
|
|
11
|
+
from api.errors import install_exception_handlers
|
|
12
|
+
from api.support import resolve_web_asset, start_limited_account_watcher
|
|
13
|
+
from services.backup_service import backup_service
|
|
14
|
+
from services.config import config
|
|
15
|
+
from services.image_service import start_image_cleanup_scheduler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_app() -> FastAPI:
|
|
19
|
+
app_version = config.app_version
|
|
20
|
+
|
|
21
|
+
@asynccontextmanager
|
|
22
|
+
async def lifespan(_: FastAPI):
|
|
23
|
+
stop_event = Event()
|
|
24
|
+
thread = start_limited_account_watcher(stop_event)
|
|
25
|
+
cleanup_thread = start_image_cleanup_scheduler(stop_event)
|
|
26
|
+
backup_service.start()
|
|
27
|
+
config.cleanup_old_images()
|
|
28
|
+
try:
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
stop_event.set()
|
|
32
|
+
thread.join(timeout=1)
|
|
33
|
+
cleanup_thread.join(timeout=1)
|
|
34
|
+
backup_service.stop()
|
|
35
|
+
|
|
36
|
+
app = FastAPI(title="chatgpt2api", version=app_version, lifespan=lifespan)
|
|
37
|
+
install_exception_handlers(app)
|
|
38
|
+
app.add_middleware(
|
|
39
|
+
CORSMiddleware,
|
|
40
|
+
allow_origins=["*"],
|
|
41
|
+
allow_credentials=False,
|
|
42
|
+
allow_methods=["*"],
|
|
43
|
+
allow_headers=["*"],
|
|
44
|
+
)
|
|
45
|
+
app.include_router(ai.create_router())
|
|
46
|
+
app.include_router(accounts.create_router())
|
|
47
|
+
app.include_router(image_tasks.create_router())
|
|
48
|
+
app.include_router(system.create_router(app_version))
|
|
49
|
+
|
|
50
|
+
@app.api_route("/{full_path:path}", methods=["GET", "HEAD"], include_in_schema=False)
|
|
51
|
+
async def serve_web(full_path: str):
|
|
52
|
+
asset = resolve_web_asset(full_path)
|
|
53
|
+
if asset is not None:
|
|
54
|
+
return FileResponse(asset)
|
|
55
|
+
if full_path.strip("/").startswith("_next/"):
|
|
56
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
57
|
+
fallback = resolve_web_asset("")
|
|
58
|
+
if fallback is None:
|
|
59
|
+
raise HTTPException(status_code=404, detail="Not Found")
|
|
60
|
+
return FileResponse(fallback)
|
|
61
|
+
|
|
62
|
+
return app
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from fastapi import FastAPI, Request
|
|
4
|
-
from fastapi.encoders import jsonable_encoder
|
|
5
|
-
from fastapi.exceptions import RequestValidationError
|
|
6
|
-
from fastapi.responses import JSONResponse
|
|
7
|
-
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
8
|
-
|
|
9
|
-
from services.protocol.error_response import anthropic_error_response, openai_error_response
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _is_openai_compatible_path(path: str) -> bool:
|
|
13
|
-
return path == "/v1" or path.startswith("/v1/")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _is_anthropic_messages_path(path: str) -> bool:
|
|
17
|
-
return path == "/v1/messages"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _compatible_error_response(
|
|
21
|
-
request: Request,
|
|
22
|
-
detail: object,
|
|
23
|
-
status_code: int,
|
|
24
|
-
headers: dict[str, str] | None = None,
|
|
25
|
-
) -> JSONResponse:
|
|
26
|
-
if _is_anthropic_messages_path(request.url.path):
|
|
27
|
-
return anthropic_error_response(detail, status_code, headers=headers)
|
|
28
|
-
return openai_error_response(detail, status_code, headers=headers)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def install_exception_handlers(app: FastAPI) -> None:
|
|
32
|
-
@app.exception_handler(StarletteHTTPException)
|
|
33
|
-
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
|
34
|
-
if _is_openai_compatible_path(request.url.path):
|
|
35
|
-
return _compatible_error_response(request, exc.detail, exc.status_code, exc.headers)
|
|
36
|
-
return JSONResponse(
|
|
37
|
-
status_code=exc.status_code,
|
|
38
|
-
content={"detail": jsonable_encoder(exc.detail)},
|
|
39
|
-
headers=exc.headers,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
@app.exception_handler(RequestValidationError)
|
|
43
|
-
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
44
|
-
if _is_openai_compatible_path(request.url.path):
|
|
45
|
-
return _compatible_error_response(request, exc.errors(), 422)
|
|
46
|
-
return JSONResponse(status_code=422, content={"detail": jsonable_encoder(exc.errors())})
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.encoders import jsonable_encoder
|
|
5
|
+
from fastapi.exceptions import RequestValidationError
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
8
|
+
|
|
9
|
+
from services.protocol.error_response import anthropic_error_response, openai_error_response
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _is_openai_compatible_path(path: str) -> bool:
|
|
13
|
+
return path == "/v1" or path.startswith("/v1/")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _is_anthropic_messages_path(path: str) -> bool:
|
|
17
|
+
return path == "/v1/messages"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _compatible_error_response(
|
|
21
|
+
request: Request,
|
|
22
|
+
detail: object,
|
|
23
|
+
status_code: int,
|
|
24
|
+
headers: dict[str, str] | None = None,
|
|
25
|
+
) -> JSONResponse:
|
|
26
|
+
if _is_anthropic_messages_path(request.url.path):
|
|
27
|
+
return anthropic_error_response(detail, status_code, headers=headers)
|
|
28
|
+
return openai_error_response(detail, status_code, headers=headers)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def install_exception_handlers(app: FastAPI) -> None:
|
|
32
|
+
@app.exception_handler(StarletteHTTPException)
|
|
33
|
+
async def http_exception_handler(request: Request, exc: StarletteHTTPException) -> JSONResponse:
|
|
34
|
+
if _is_openai_compatible_path(request.url.path):
|
|
35
|
+
return _compatible_error_response(request, exc.detail, exc.status_code, exc.headers)
|
|
36
|
+
return JSONResponse(
|
|
37
|
+
status_code=exc.status_code,
|
|
38
|
+
content={"detail": jsonable_encoder(exc.detail)},
|
|
39
|
+
headers=exc.headers,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@app.exception_handler(RequestValidationError)
|
|
43
|
+
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
44
|
+
if _is_openai_compatible_path(request.url.path):
|
|
45
|
+
return _compatible_error_response(request, exc.errors(), 422)
|
|
46
|
+
return JSONResponse(status_code=422, content={"detail": jsonable_encoder(exc.errors())})
|