传sir commited on
Commit
162b03e
·
1 Parent(s): be1ae85
.arts/settings.json ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ {
2
+ "clawMode.mode": "editor",
3
+ "workbench.activityBar.location": "default"
4
+ }
.codeartsdoer/.codebaseignore ADDED
File without changes
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.13-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY app.py .
9
+
10
+ ENV PORT=7860
11
+ EXPOSE 7860
12
+
13
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -3,10 +3,8 @@ title: Modelscope Api
3
  emoji: 🏢
4
  colorFrom: purple
5
  colorTo: gray
6
- sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
- app_file: app.py
10
  pinned: false
11
  ---
12
 
 
3
  emoji: 🏢
4
  colorFrom: purple
5
  colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
 
 
8
  pinned: false
9
  ---
10
 
__pycache__/app.cpython-310.pyc ADDED
Binary file (7.75 kB). View file
 
app.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import time
4
+ from typing import Dict, Optional
5
+
6
+ import aiohttp
7
+ from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.responses import HTMLResponse, Response, StreamingResponse
9
+
10
+
11
+ logger = logging.getLogger("modelscope_proxy")
12
+ logging.basicConfig(level=os.getenv("LOG_LEVEL", "INFO").upper())
13
+
14
+ MODELSCOPE_COOKIE = os.getenv("MODELSCOPE_COOKIE", "")
15
+ TOKEN_TTL_SECONDS = int(os.getenv("MODELSCOPE_TOKEN_TTL_SECONDS", "3300"))
16
+
17
+ TOKEN_URL = "https://modelscope.cn/api/v1/studios/token"
18
+
19
+ ROUTES = {
20
+ "/image": {
21
+ "url": "https://chuansir-qwen-image.ms.show/image",
22
+ "methods": {"POST"},
23
+ },
24
+ "/edit-image": {
25
+ "url": "https://chuansir-qwen-image.ms.show/edit-image",
26
+ "methods": {"POST"},
27
+ },
28
+ "/v1/chat/completions": {
29
+ "url": "https://chuansir-qwen3-5-27b-claude-4-6-opus-reasoning-dis.ms.show/v1/chat/completions",
30
+ "methods": {"POST"},
31
+ },
32
+ "/v1/messages": {
33
+ "url": "https://chuansir-qwen3-5-27b-claude-4-6-opus-reasoning-dis.ms.show/v1/messages",
34
+ "methods": {"POST"},
35
+ },
36
+ "/v1/models": {
37
+ "url": "https://chuansir-qwen3-5-27b-claude-4-6-opus-reasoning-dis.ms.show/v1/models",
38
+ "methods": {"GET"},
39
+ },
40
+ "/tts": {
41
+ "url": "https://chuansir-index-tts-vllm.ms.show/tts",
42
+ "methods": {"GET", "POST"},
43
+ },
44
+ "/stt/transcribe": {
45
+ "url": "https://chuansir-index-tts-vllm.ms.show/stt/transcribe",
46
+ "methods": {"POST"},
47
+ },
48
+ }
49
+
50
+ HOP_BY_HOP_HEADERS = {
51
+ "connection",
52
+ "keep-alive",
53
+ "proxy-authenticate",
54
+ "proxy-authorization",
55
+ "te",
56
+ "trailers",
57
+ "transfer-encoding",
58
+ "upgrade",
59
+ "host",
60
+ "content-length",
61
+ }
62
+
63
+ app = FastAPI(title="ModelScope Studio API Proxy")
64
+
65
+
66
+ class ModelScopeTokenCache:
67
+ def __init__(self, cookie: str, ttl_seconds: int) -> None:
68
+ self.cookie = cookie
69
+ self.ttl_seconds = ttl_seconds
70
+ self._token: Optional[str] = None
71
+ self._expires_at = 0.0
72
+
73
+ async def get(self, session: aiohttp.ClientSession) -> str:
74
+ now = time.time()
75
+ if self._token and now < self._expires_at:
76
+ return self._token
77
+
78
+ token = await self._get_modelscope_token(session, self._token_headers())
79
+ self._token = token
80
+ self._expires_at = now + self.ttl_seconds
81
+ return token
82
+
83
+ async def _get_modelscope_token(
84
+ self, session: aiohttp.ClientSession, headers: Dict[str, str]
85
+ ) -> str:
86
+ async with session.get(TOKEN_URL, headers=headers) as response:
87
+ res_text = await response.text()
88
+ logger.debug(res_text)
89
+ response.raise_for_status()
90
+
91
+ token_data = await response.json()
92
+ return token_data["Data"]["Token"]
93
+
94
+ def _token_headers(self) -> Dict[str, str]:
95
+ if not self.cookie:
96
+ raise HTTPException(
97
+ status_code=500,
98
+ detail="MODELSCOPE_COOKIE environment variable is not set",
99
+ )
100
+
101
+ return {
102
+ "User-Agent": (
103
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
104
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
105
+ "Chrome/121.0.0.0 Safari/537.36"
106
+ ),
107
+ "Cookie": self.cookie,
108
+ }
109
+
110
+
111
+ token_cache = ModelScopeTokenCache(MODELSCOPE_COOKIE, TOKEN_TTL_SECONDS)
112
+
113
+
114
+ @app.get("/", response_class=HTMLResponse)
115
+ async def index() -> str:
116
+ return """
117
+ <!doctype html>
118
+ <html>
119
+ <head>
120
+ <meta charset="utf-8" />
121
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
122
+ <title>ModelScope API Proxy</title>
123
+ <style>
124
+ body { font-family: system-ui, sans-serif; max-width: 860px; margin: 40px auto; padding: 0 20px; line-height: 1.5; }
125
+ code { background: #f4f4f5; padding: 2px 5px; border-radius: 4px; }
126
+ li { margin: 6px 0; }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <h1>ModelScope API Proxy</h1>
131
+ <p>Proxy is running. See <a href="/docs">/docs</a> for OpenAPI docs.</p>
132
+ <ul>
133
+ <li><code>POST /image</code></li>
134
+ <li><code>POST /edit-image</code></li>
135
+ <li><code>POST /v1/chat/completions</code></li>
136
+ <li><code>POST /v1/messages</code></li>
137
+ <li><code>GET /v1/models</code></li>
138
+ <li><code>GET|POST /tts</code></li>
139
+ <li><code>POST /stt/transcribe</code></li>
140
+ </ul>
141
+ </body>
142
+ </html>
143
+ """
144
+
145
+
146
+ @app.get("/health")
147
+ async def health() -> Dict[str, str]:
148
+ return {"status": "ok"}
149
+
150
+
151
+ @app.on_event("startup")
152
+ async def startup() -> None:
153
+ timeout = aiohttp.ClientTimeout(total=None, sock_connect=60, sock_read=None)
154
+ app.state.session = aiohttp.ClientSession(timeout=timeout)
155
+
156
+
157
+ @app.on_event("shutdown")
158
+ async def shutdown() -> None:
159
+ await app.state.session.close()
160
+
161
+
162
+ @app.api_route(
163
+ "/{path:path}",
164
+ methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
165
+ )
166
+ async def proxy(path: str, request: Request) -> Response:
167
+ route_path = "/" + path
168
+ route = ROUTES.get(route_path)
169
+ if not route:
170
+ raise HTTPException(status_code=404, detail="Proxy route not found")
171
+
172
+ if request.method not in route["methods"]:
173
+ raise HTTPException(status_code=405, detail="Method not allowed")
174
+
175
+ session: aiohttp.ClientSession = app.state.session
176
+ token = await token_cache.get(session)
177
+ headers = _build_forward_headers(request, token)
178
+ body = await request.body()
179
+
180
+ try:
181
+ upstream = await session.request(
182
+ method=request.method,
183
+ url=route["url"],
184
+ params=request.query_params,
185
+ headers=headers,
186
+ data=body if body else None,
187
+ )
188
+ except aiohttp.ClientError as exc:
189
+ logger.exception("Upstream request failed")
190
+ raise HTTPException(status_code=502, detail=str(exc)) from exc
191
+
192
+ response_headers = _build_response_headers(upstream.headers)
193
+
194
+ if _should_stream(upstream):
195
+ return StreamingResponse(
196
+ upstream.content.iter_chunked(64 * 1024),
197
+ status_code=upstream.status,
198
+ headers=response_headers,
199
+ media_type=upstream.headers.get("content-type"),
200
+ background=_CloseAiohttpResponse(upstream),
201
+ )
202
+
203
+ content = await upstream.read()
204
+ upstream.release()
205
+ return Response(
206
+ content=content,
207
+ status_code=upstream.status,
208
+ headers=response_headers,
209
+ media_type=upstream.headers.get("content-type"),
210
+ )
211
+
212
+
213
+ def _build_forward_headers(request: Request, token: str) -> Dict[str, str]:
214
+ headers = {
215
+ key: value
216
+ for key, value in request.headers.items()
217
+ if key.lower() not in HOP_BY_HOP_HEADERS
218
+ }
219
+ headers["x-studio-token"] = token
220
+ return headers
221
+
222
+
223
+ def _build_response_headers(headers: aiohttp.typedefs.LooseHeaders) -> Dict[str, str]:
224
+ return {
225
+ key: value
226
+ for key, value in headers.items()
227
+ if key.lower() not in HOP_BY_HOP_HEADERS
228
+ }
229
+
230
+
231
+ def _should_stream(response: aiohttp.ClientResponse) -> bool:
232
+ content_type = response.headers.get("content-type", "").lower()
233
+ return (
234
+ "text/event-stream" in content_type
235
+ or "application/octet-stream" in content_type
236
+ or response.headers.get("transfer-encoding", "").lower() == "chunked"
237
+ )
238
+
239
+
240
+ class _CloseAiohttpResponse:
241
+ def __init__(self, response: aiohttp.ClientResponse) -> None:
242
+ self.response = response
243
+
244
+ async def __call__(self) -> None:
245
+ self.response.release()
246
+
247
+
248
+ if __name__ == "__main__":
249
+ import uvicorn
250
+
251
+ port = int(os.getenv("PORT", "7860"))
252
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ aiohttp
2
+ fastapi
3
+ uvicorn[standard]