Bc-AI commited on
Commit
b9b4453
Β·
verified Β·
1 Parent(s): 193fa16

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +231 -44
app.py CHANGED
@@ -1,72 +1,259 @@
1
- from flask import Flask, request, Response, jsonify
2
- from flask_cors import CORS
3
- import requests
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
 
6
  app = Flask(__name__)
7
  CORS(app, origins=["*"])
8
 
9
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
 
 
10
 
11
- # Map each model key to its real HF endpoint. Add new models here only.
12
- ENDPOINTS = {
13
- "mira-1-large": "https://ixvuours3f75nti4.us-east-1.aws.endpoints.huggingface.cloud/v1/",
14
- "mira-1-xl": "https://uk68x3qqblj2iz7i.us-east-1.aws.endpoints.huggingface.cloud/v1/",
 
 
 
15
  }
16
 
17
- def proxy_request(endpoint_url, data):
18
- headers = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "Authorization": f"Bearer {HF_TOKEN}",
20
- "Content-Type": "application/json"
 
21
  }
22
 
23
- if data.get('stream', False):
24
- resp = requests.post(
25
- f"{endpoint_url}chat/completions",
 
 
 
 
 
 
 
 
 
 
 
26
  json=data,
27
- headers=headers,
28
  stream=True,
29
- timeout=300
30
  )
 
31
 
32
  def generate():
33
- for chunk in resp.iter_content(chunk_size=None, decode_unicode=False):
34
- if chunk:
35
- yield chunk
36
-
37
- return Response(generate(), content_type='text/event-stream')
38
- else:
39
- resp = requests.post(
40
- f"{endpoint_url}chat/completions",
41
- json=data,
42
- headers=headers,
43
- timeout=60
 
 
 
 
 
 
 
 
44
  )
45
- return jsonify(resp.json())
46
 
47
- # Original route β€” kept as-is so existing Mira-1-Large calls with no path change still work
48
- @app.route('/v1/chat/completions', methods=['POST'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  def chat_completions_default():
50
- data = request.json
 
 
 
 
 
 
 
51
  try:
52
- return proxy_request(ENDPOINTS["mira-1-large"], data)
 
 
 
 
 
53
  except Exception as e:
54
- return jsonify({"error": str(e)}), 500
 
55
 
56
- # New: per-model route, e.g. /v1/mira-1-xl/chat/completions
57
- @app.route('/v1/<model_key>/chat/completions', methods=['POST'])
58
- def chat_completions_by_model(model_key):
 
 
 
 
 
59
  if model_key not in ENDPOINTS:
60
- return jsonify({"error": f"Unknown model '{model_key}'"}), 400
61
- data = request.json
 
 
 
 
62
  try:
63
- return proxy_request(ENDPOINTS[model_key], data)
 
 
 
 
 
64
  except Exception as e:
65
- return jsonify({"error": str(e)}), 500
 
66
 
67
- @app.route('/health', methods=['GET'])
 
68
  def health():
69
- return jsonify({"status": "ok", "endpoint": "mira-proxy", "models": list(ENDPOINTS.keys())})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- if __name__ == '__main__':
72
- app.run(host='0.0.0.0', port=7860)
 
 
 
1
+ # ─────────────────────────────────────────────────────────────────────────────
2
+ # Mira Proxy Space β€’ Docker SDK β€’ No GPU needed
3
+ #
4
+ # Routes:
5
+ # POST /v1/chat/completions β†’ mira-1-large (default)
6
+ # POST /v1/{model_key}/chat/completions β†’ any registered model
7
+ # GET /v1/models β†’ list available models
8
+ # GET /health β†’ proxy + upstream health
9
+ # GET / β†’ info page
10
+ #
11
+ # Env secrets (set in Space settings):
12
+ # HF_TOKEN β€” used both to call upstream spaces AND to optionally
13
+ # gate inbound requests (if AUTH_REQUIRED=true)
14
+ # AUTH_REQUIRED β€” set to "true" to require Bearer token on inbound calls
15
+ # ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ from __future__ import annotations
18
+
19
  import os
20
+ import time
21
+ import logging
22
+ from functools import wraps
23
+
24
+ import requests
25
+ from flask import Flask, request, Response, jsonify, abort
26
+ from flask_cors import CORS
27
+
28
+ # ── Logging ───────────────────────────────────────────────────────────────────
29
+ logging.basicConfig(
30
+ level=logging.INFO,
31
+ format="%(asctime)s [%(levelname)s] %(message)s",
32
+ )
33
+ log = logging.getLogger(__name__)
34
 
35
+ # ── App ───────────────────────────────────────────────────────────────────────
36
  app = Flask(__name__)
37
  CORS(app, origins=["*"])
38
 
39
+ # ── Config ────────────────────────────────────────────────────────────────────
40
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
41
+ AUTH_REQUIRED = os.environ.get("AUTH_REQUIRED", "false").lower() == "true"
42
 
43
+ # ── Endpoint registry ─────────────────────────────────────────────────────────
44
+ # Each value is the Space root URL (no trailing path).
45
+ # The proxy appends /v1/chat/completions when forwarding.
46
+ # Update these slugs to match your actual Space URLs.
47
+ ENDPOINTS: dict[str, str] = {
48
+ "mira-1-large": "https://ml-intern-explorers-inf-end-1.hf.space",
49
+ "mira-1-xl": "https://ml-intern-explorers-inf-end-2.hf.space",
50
  }
51
 
52
+ DEFAULT_MODEL = "mira-1-large"
53
+
54
+ # Model metadata for /v1/models response (OpenAI-compatible)
55
+ MODEL_META = {
56
+ "mira-1-large": {"description": "Mira 1 Large", "context_window": 8192},
57
+ "mira-1-xl": {"description": "Mira 1 XL", "context_window": 16384},
58
+ }
59
+
60
+ # ── Auth guard (optional) ─────────────────────────────────────────────────────
61
+ def require_auth(f):
62
+ @wraps(f)
63
+ def decorated(*args, **kwargs):
64
+ if not AUTH_REQUIRED:
65
+ return f(*args, **kwargs)
66
+ auth = request.headers.get("Authorization", "")
67
+ if not auth.startswith("Bearer ") or auth.split(" ", 1)[1] != HF_TOKEN:
68
+ return jsonify({"error": "Unauthorized"}), 401
69
+ return f(*args, **kwargs)
70
+ return decorated
71
+
72
+ # ── Upstream headers ──────────────────────────────────────────────────────────
73
+ def _upstream_headers() -> dict:
74
+ return {
75
  "Authorization": f"Bearer {HF_TOKEN}",
76
+ "Content-Type": "application/json",
77
+ "Accept": "application/json",
78
  }
79
 
80
+ # ── Core proxy logic ──────────────────────────────────────────────────────────
81
+ def proxy_request(model_key: str, data: dict) -> Response:
82
+ base = ENDPOINTS[model_key].rstrip("/")
83
+ target = f"{base}/v1/chat/completions"
84
+
85
+ # Ensure the model field in the payload matches what the Space expects
86
+ data.setdefault("model", model_key)
87
+
88
+ log.info(f"β†’ {model_key} stream={data.get('stream', False)}")
89
+
90
+ if data.get("stream", False):
91
+ # ── Streaming: forward SSE chunks as they arrive ──────────────────────
92
+ upstream = requests.post(
93
+ target,
94
  json=data,
95
+ headers=_upstream_headers(),
96
  stream=True,
97
+ timeout=(10, 300), # (connect, read)
98
  )
99
+ upstream.raise_for_status()
100
 
101
  def generate():
102
+ try:
103
+ for chunk in upstream.iter_content(
104
+ chunk_size=None, decode_unicode=False
105
+ ):
106
+ if chunk:
107
+ yield chunk
108
+ except Exception as exc:
109
+ log.error(f"Streaming error for {model_key}: {exc}")
110
+ yield b"data: [DONE]\n\n"
111
+
112
+ return Response(
113
+ generate(),
114
+ status=upstream.status_code,
115
+ content_type="text/event-stream",
116
+ headers={
117
+ "Cache-Control": "no-cache",
118
+ "X-Accel-Buffering":"no", # disable nginx buffering
119
+ "Connection": "keep-alive",
120
+ },
121
  )
 
122
 
123
+ # ── Non-streaming ─────────────────────────────────────────────────────────
124
+ upstream = requests.post(
125
+ target,
126
+ json=data,
127
+ headers=_upstream_headers(),
128
+ timeout=(10, 60),
129
+ )
130
+ upstream.raise_for_status()
131
+ return jsonify(upstream.json())
132
+
133
+
134
+ # ── Routes ────────────────────────────────────────────────────────────────────
135
+
136
+ @app.route("/", methods=["GET"])
137
+ def index():
138
+ """Human-readable info page."""
139
+ return jsonify({
140
+ "service": "Mira Proxy",
141
+ "version": "1.0.0",
142
+ "models": list(ENDPOINTS.keys()),
143
+ "default_model": DEFAULT_MODEL,
144
+ "endpoints": {
145
+ "chat": "POST /v1/chat/completions",
146
+ "models": "GET /v1/models",
147
+ "health": "GET /health",
148
+ },
149
+ })
150
+
151
+
152
+ @app.route("/v1/models", methods=["GET"])
153
+ @require_auth
154
+ def list_models():
155
+ """OpenAI-compatible model listing."""
156
+ now = int(time.time())
157
+ return jsonify({
158
+ "object": "list",
159
+ "data": [
160
+ {
161
+ "id": key,
162
+ "object": "model",
163
+ "created": now,
164
+ "owned_by": "mira",
165
+ **MODEL_META.get(key, {}),
166
+ }
167
+ for key in ENDPOINTS
168
+ ],
169
+ })
170
+
171
+
172
+ @app.route("/v1/chat/completions", methods=["POST"])
173
+ @require_auth
174
  def chat_completions_default():
175
+ """
176
+ Default route β€” always routes to mira-1-large.
177
+ Existing callers need zero changes.
178
+ """
179
+ data = request.json or {}
180
+ # If caller already specifies a known model in the payload, honour it.
181
+ requested = data.get("model", DEFAULT_MODEL)
182
+ model_key = requested if requested in ENDPOINTS else DEFAULT_MODEL
183
  try:
184
+ return proxy_request(model_key, data)
185
+ except requests.HTTPError as e:
186
+ log.error(f"Upstream HTTP error: {e}")
187
+ return jsonify({"error": str(e)}), e.response.status_code
188
+ except requests.Timeout:
189
+ return jsonify({"error": "upstream timeout"}), 504
190
  except Exception as e:
191
+ log.error(f"Proxy error: {e}")
192
+ return jsonify({"error": str(e)}), 502
193
 
194
+
195
+ @app.route("/v1/<model_key>/chat/completions", methods=["POST"])
196
+ @require_auth
197
+ def chat_completions_by_model(model_key: str):
198
+ """
199
+ Per-model route.
200
+ e.g. POST /v1/mira-1-xl/chat/completions
201
+ """
202
  if model_key not in ENDPOINTS:
203
+ return jsonify({
204
+ "error": f"Unknown model '{model_key}'",
205
+ "available_models": list(ENDPOINTS.keys()),
206
+ }), 404
207
+
208
+ data = request.json or {}
209
  try:
210
+ return proxy_request(model_key, data)
211
+ except requests.HTTPError as e:
212
+ log.error(f"Upstream HTTP error ({model_key}): {e}")
213
+ return jsonify({"error": str(e)}), e.response.status_code
214
+ except requests.Timeout:
215
+ return jsonify({"error": "upstream timeout"}), 504
216
  except Exception as e:
217
+ log.error(f"Proxy error ({model_key}): {e}")
218
+ return jsonify({"error": str(e)}), 502
219
 
220
+
221
+ @app.route("/health", methods=["GET"])
222
  def health():
223
+ """
224
+ Proxy liveness + optional upstream health checks.
225
+ Pass ?upstream=true to also ping each Space's /health endpoint.
226
+ """
227
+ result: dict = {
228
+ "status": "ok",
229
+ "service": "mira-proxy",
230
+ "models": list(ENDPOINTS.keys()),
231
+ "upstream": {},
232
+ }
233
+
234
+ if request.args.get("upstream", "false").lower() == "true":
235
+ for key, base in ENDPOINTS.items():
236
+ url = f"{base.rstrip('/')}/health"
237
+ try:
238
+ r = requests.get(
239
+ url,
240
+ headers={"Authorization": f"Bearer {HF_TOKEN}"},
241
+ timeout=8,
242
+ )
243
+ result["upstream"][key] = {
244
+ "status": "ok" if r.ok else "error",
245
+ "http_status": r.status_code,
246
+ "body": r.json() if r.ok else r.text[:200],
247
+ }
248
+ except requests.Timeout:
249
+ result["upstream"][key] = {"status": "timeout"}
250
+ except Exception as exc:
251
+ result["upstream"][key] = {"status": "error", "detail": str(exc)}
252
+
253
+ return jsonify(result)
254
+
255
 
256
+ # ── Dev entrypoint ────────────────────────────────────────────────────────────
257
+ if __name__ == "__main__":
258
+ # gunicorn is used in production (see Dockerfile CMD)
259
+ app.run(host="0.0.0.0", port=7860, debug=False)