zhidong010 commited on
Commit
7eb5198
·
verified ·
1 Parent(s): 33fec9f

Upload 3 files

Browse files
Files changed (3) hide show
  1. 2api.py +491 -0
  2. Dockerfile +20 -0
  3. requirements.txt +2 -0
2api.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import base64
4
+ from typing import Dict, Optional
5
+ from flask import Flask, request, Response, stream_with_context
6
+ import os
7
+ import time
8
+ from datetime import datetime, timedelta
9
+
10
+ # Initialize Flask app
11
+ app = Flask(__name__)
12
+
13
+ # Load configuration from config.json if it exists, otherwise use environment variables
14
+ CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')
15
+ if os.path.exists(CONFIG_FILE):
16
+ with open(CONFIG_FILE, 'r') as f:
17
+ config = json.load(f)
18
+ ACCOUNTS = config.get('accounts', [])
19
+ else:
20
+ ACCOUNTS = []
21
+ accounts_env = os.getenv("ONDEMAND_ACCOUNTS", "")
22
+ if accounts_env:
23
+ try:
24
+ ACCOUNTS = json.loads(accounts_env).get('accounts', [])
25
+ except json.JSONDecodeError:
26
+ print("Error decoding ONDEMAND_ACCOUNTS environment variable. Using empty accounts list.")
27
+
28
+ if not ACCOUNTS:
29
+ raise ValueError("No accounts found in config.json or environment variable ONDEMAND_ACCOUNTS.")
30
+
31
+ # Current account index (for round-robin selection)
32
+ current_account_index = 0
33
+
34
+ # In-memory storage for session and last interaction time per client
35
+ CLIENT_SESSIONS = {} # Format: {client_id: {"session_id": str, "last_time": datetime, "user_id": str, "company_id": str, "token": str}}
36
+
37
+
38
+ class OnDemandAPIClient:
39
+ def __init__(self, email: str, password: str):
40
+ self.email = email
41
+ self.password = password
42
+ self.token = ""
43
+ self.refresh_token = ""
44
+ self.user_id = ""
45
+ self.company_id = ""
46
+ self.session_id = ""
47
+ self.base_url = "https://gateway.on-demand.io/v1"
48
+ self.chat_base_url = "https://api.on-demand.io/chat/v1/client"
49
+
50
+ def get_authorization(self) -> str:
51
+ """Generate Basic Authorization header for login."""
52
+ text = f"{self.email}:{self.password}"
53
+ encoded = base64.b64encode(text.encode("utf-8")).decode("utf-8")
54
+ return encoded
55
+
56
+ def sign_in(self) -> bool:
57
+ """Login to get token, refreshToken, userId, and companyId."""
58
+ url = f"{self.base_url}/auth/user/signin"
59
+ payload = {
60
+ "accountType": "default"
61
+ }
62
+ headers = {
63
+ 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36 Edg/135.0.0.0",
64
+ 'Accept': "application/json, text/plain, */*",
65
+ 'Accept-Encoding': "gzip, deflate, br, zstd",
66
+ 'Content-Type': "application/json",
67
+ 'Authorization': f"Basic {self.get_authorization()}",
68
+ 'Referer': "https://app.on-demand.io/"
69
+ }
70
+
71
+ try:
72
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
73
+ response.raise_for_status()
74
+ data = response.json()
75
+ print("Raw response from sign_in:", json.dumps(data, indent=2))
76
+ self.token = data.get('data', {}).get('tokenData', {}).get('token', '')
77
+ self.refresh_token = data.get('data', {}).get('tokenData', {}).get('refreshToken', '')
78
+ self.user_id = data.get('data', {}).get('user', {}).get('userId', '')
79
+ self.company_id = data.get('data', {}).get('user', {}).get('default_company_id', '')
80
+ print(f"Extracted Token: {self.token[:10]}... (truncated for security)")
81
+ print(f"Extracted Refresh Token: {self.refresh_token[:10]}... (truncated for security)")
82
+ print(f"Extracted User ID: {self.user_id}")
83
+ print(f"Extracted Company ID: {self.company_id}")
84
+ if self.token and self.user_id and self.company_id:
85
+ print(f"Login successful for {self.email}. Token and user info retrieved.")
86
+ return True
87
+ else:
88
+ print("Login successful but failed to extract required fields.")
89
+ return False
90
+ except requests.exceptions.RequestException as e:
91
+ print(f"Login failed for {self.email}: {e}")
92
+ return False
93
+
94
+ def refresh_token_if_needed(self) -> bool:
95
+ """Refresh token if it is expired or invalid."""
96
+ if not self.token or not self.refresh_token:
97
+ print("No token or refresh token available. Please log in first.")
98
+ return False
99
+
100
+ url = f"{self.base_url}/auth/user/refresh_token"
101
+ payload = {
102
+ "data": {
103
+ "token": self.token,
104
+ "refreshToken": self.refresh_token
105
+ }
106
+ }
107
+ headers = {
108
+ 'Content-Type': "application/json"
109
+ }
110
+
111
+ try:
112
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
113
+ response.raise_for_status()
114
+ data = response.json()
115
+ print("Raw response from refresh_token:", json.dumps(data, indent=2))
116
+ self.token = data.get('data', {}).get('token', '')
117
+ self.refresh_token = data.get('data', {}).get('refreshToken', '')
118
+ print(f"New Token: {self.token[:10]}... (truncated for security)")
119
+ print("Token refreshed successfully.")
120
+ return True
121
+ except requests.exceptions.RequestException as e:
122
+ print(f"Token refresh failed: {e}")
123
+ return False
124
+
125
+ def create_session(self, external_user_id: str = "user-app-12345") -> Optional[str]:
126
+ """Create a new session for chat."""
127
+ if not self.token or not self.user_id or not self.company_id:
128
+ print("No token or user info available. Please log in or refresh token.")
129
+ return None
130
+
131
+ url = f"{self.chat_base_url}/sessions"
132
+ payload = {
133
+ "externalUserId": external_user_id,
134
+ "pluginIds": []
135
+ }
136
+ headers = {
137
+ 'Content-Type': "application/json",
138
+ 'Authorization': f"Bearer {self.token}",
139
+ 'x-company-id': self.company_id,
140
+ 'x-user-id': self.user_id
141
+ }
142
+ print(f"Creating session with company_id: {self.company_id}, user_id: {self.user_id}")
143
+
144
+ try:
145
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
146
+ if response.status_code == 401:
147
+ print("Token expired, refreshing...")
148
+ if self.refresh_token_if_needed():
149
+ headers['Authorization'] = f"Bearer {self.token}"
150
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
151
+ response.raise_for_status()
152
+ data = response.json()
153
+ print("Raw response from create_session:", json.dumps(data, indent=2))
154
+ self.session_id = data.get('data', {}).get('id', '')
155
+ print(f"Session created successfully. Session ID: {self.session_id}")
156
+ return self.session_id
157
+ except requests.exceptions.RequestException as e:
158
+ print(f"Session creation failed: {e}")
159
+ return None
160
+
161
+ def send_query(self, query: str, endpoint_id: str = "predefined-claude-3.7-sonnet", stream: bool = False) -> Dict:
162
+ """Send a query to the chat session and handle streaming or non-streaming response."""
163
+ if not self.session_id or not self.token:
164
+ print("No session ID or token available. Please create a session first.")
165
+ return {"error": "No session or token available"}
166
+
167
+ url = f"{self.chat_base_url}/sessions/{self.session_id}/query"
168
+ payload = {
169
+ "endpointId": endpoint_id,
170
+ "query": query,
171
+ "pluginIds": [],
172
+ "reasoningMode": "high",
173
+ "responseMode": "stream" if stream else "sync",
174
+ "debugMode": "on",
175
+ "modelConfigs": {
176
+ "fulfillmentPrompt": "",
177
+ "stopTokens": [],
178
+ "maxTokens": 0,
179
+ "temperature": 0,
180
+ "presencePenalty": 0,
181
+ "frequencyPenalty": 0,
182
+ "topP": 1
183
+ },
184
+ "fulfillmentOnly": False
185
+ }
186
+ headers = {
187
+ 'Content-Type': "application/json",
188
+ 'Authorization': f"Bearer {self.token}",
189
+ 'x-company-id': self.company_id
190
+ }
191
+
192
+ try:
193
+ if stream:
194
+ response = requests.post(url, data=json.dumps(payload), headers=headers, stream=True)
195
+ if response.status_code == 401:
196
+ print("Token expired, refreshing...")
197
+ if self.refresh_token_if_needed():
198
+ headers['Authorization'] = f"Bearer {self.token}"
199
+ response = requests.post(url, data=json.dumps(payload), headers=headers, stream=True)
200
+ response.raise_for_status()
201
+ return {"stream": True, "response": response}
202
+ else:
203
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
204
+ if response.status_code == 401:
205
+ print("Token expired, refreshing...")
206
+ if self.refresh_token_if_needed():
207
+ headers['Authorization'] = f"Bearer {self.token}"
208
+ response = requests.post(url, data=json.dumps(payload), headers=headers)
209
+ response.raise_for_status()
210
+ full_answer = ""
211
+ for line in response.iter_lines():
212
+ if line:
213
+ decoded_line = line.decode('utf-8')
214
+ if decoded_line.startswith("data:"):
215
+ json_str = decoded_line[len("data:"):]
216
+ if json_str == "[DONE]":
217
+ break
218
+ try:
219
+ event_data = json.loads(json_str)
220
+ if event_data.get("eventType", "") == "fulfillment":
221
+ full_answer += event_data.get("answer", "")
222
+ except json.JSONDecodeError:
223
+ continue
224
+ return {"stream": False, "content": full_answer}
225
+ except requests.exceptions.RequestException as e:
226
+ print(f"Query failed: {e}")
227
+ return {"error": str(e)}
228
+
229
+
230
+ # Initialize the first client with the first account
231
+ def get_next_client():
232
+ global current_account_index
233
+ account = ACCOUNTS[current_account_index]
234
+ email = account.get('email')
235
+ password = account.get('password')
236
+ print(f"Using account: {email}")
237
+ current_account_index = (current_account_index + 1) % len(ACCOUNTS) # Round-robin to next account
238
+ return OnDemandAPIClient(email, password)
239
+
240
+
241
+ # Current client (will be replaced when switching accounts)
242
+ current_client = get_next_client()
243
+
244
+ # Global variable to track initialization
245
+ initialized = False
246
+
247
+
248
+ @app.before_request
249
+ def initialize_client():
250
+ global initialized, current_client
251
+ if not initialized:
252
+ if current_client.sign_in():
253
+ current_client.create_session()
254
+ initialized = True
255
+ else:
256
+ print("Initialization failed. Switching to next account.")
257
+ current_client = get_next_client()
258
+ initialize_client() # Recursive call with new client
259
+
260
+
261
+ @app.route('/v1/models', methods=['GET'])
262
+ def get_models():
263
+ """Return a list of available models in OpenAI format."""
264
+ models_response = {
265
+ "object": "list",
266
+ "data": [
267
+ {
268
+ "id": "gpto3-mini",
269
+ "object": "model",
270
+ "created": int(time.time()),
271
+ "owned_by": "on-demand.io"
272
+ },
273
+ {
274
+ "id": "gpt-4o",
275
+ "object": "model",
276
+ "created": int(time.time()),
277
+ "owned_by": "on-demand.io"
278
+ },
279
+ {
280
+ "id": "gpt-4.1",
281
+ "object": "model",
282
+ "created": int(time.time()),
283
+ "owned_by": "on-demand.io"
284
+ },
285
+ {
286
+ "id": "gpt-4.1-mini",
287
+ "object": "model",
288
+ "created": int(time.time()),
289
+ "owned_by": "on-demand.io"
290
+ },
291
+ {
292
+ "id": "gpt-4.1-nano",
293
+ "object": "model",
294
+ "created": int(time.time()),
295
+ "owned_by": "on-demand.io"
296
+ },
297
+ {
298
+ "id": "gpt-4o-mini",
299
+ "object": "model",
300
+ "created": int(time.time()),
301
+ "owned_by": "on-demand.io"
302
+ },
303
+ {
304
+ "id": "deepseek-v3",
305
+ "object": "model",
306
+ "created": int(time.time()),
307
+ "owned_by": "on-demand.io"
308
+ },
309
+ {
310
+ "id": "deepseek-r1",
311
+ "object": "model",
312
+ "created": int(time.time()),
313
+ "owned_by": "on-demand.io"
314
+ },
315
+ {
316
+ "id": "claude-3.7-sonnet",
317
+ "object": "model",
318
+ "created": int(time.time()),
319
+ "owned_by": "on-demand.io"
320
+ },
321
+ {
322
+ "id": "gemini-2.0-flash",
323
+ "object": "model",
324
+ "created": int(time.time()),
325
+ "owned_by": "on-demand.io"
326
+ }
327
+ ]
328
+ }
329
+ return models_response
330
+
331
+
332
+ @app.route('/v1/chat/completions', methods=['POST'])
333
+ def chat_completions():
334
+ global current_client
335
+ data = request.get_json()
336
+ print("Received OpenAI request:", json.dumps(data, indent=2))
337
+
338
+ # Extract client ID (use IP address as a simple identifier for different clients)
339
+ client_id = request.remote_addr # Alternatively, use a unique ID from request if provided by Cherry Studio
340
+
341
+ # Check last interaction time for this client
342
+ current_time = datetime.now()
343
+ if client_id in CLIENT_SESSIONS:
344
+ last_time = CLIENT_SESSIONS[client_id].get("last_time")
345
+ if last_time and current_time - last_time > timedelta(minutes=10):
346
+ print(f"Client {client_id} inactive for over 10 minutes. Switching session or account.")
347
+ # Option 1: Create new session with current account
348
+ new_session = current_client.create_session()
349
+ if new_session:
350
+ CLIENT_SESSIONS[client_id]["session_id"] = new_session
351
+ print(f"New session created for client {client_id}: {new_session}")
352
+ else:
353
+ # Option 2: If session creation fails, switch account
354
+ print("Failed to create new session. Switching to next account.")
355
+ current_client = get_next_client()
356
+ if current_client.sign_in():
357
+ new_session = current_client.create_session()
358
+ if new_session:
359
+ CLIENT_SESSIONS[client_id] = {
360
+ "session_id": new_session,
361
+ "last_time": current_time,
362
+ "user_id": current_client.user_id,
363
+ "company_id": current_client.company_id,
364
+ "token": current_client.token
365
+ }
366
+ print(f"Switched account and created new session for client {client_id}: {new_session}")
367
+ else:
368
+ return {"error": "Failed to create session with new account"}, 500
369
+ else:
370
+ return {"error": "Failed to login with new account"}, 500
371
+ else:
372
+ # New client, use current session or create one
373
+ if not current_client.session_id:
374
+ if not current_client.sign_in() or not current_client.create_session():
375
+ return {"error": "Failed to initialize client session"}, 500
376
+ CLIENT_SESSIONS[client_id] = {
377
+ "session_id": current_client.session_id,
378
+ "last_time": current_time,
379
+ "user_id": current_client.user_id,
380
+ "company_id": current_client.company_id,
381
+ "token": current_client.token
382
+ }
383
+
384
+ # Update last interaction time
385
+ CLIENT_SESSIONS[client_id]["last_time"] = current_time
386
+
387
+ # Extract parameters from OpenAI request
388
+ messages = data.get('messages', [])
389
+ stream = data.get('stream', False)
390
+ model = data.get('model', 'claude-3.7-sonnet')
391
+
392
+ if not messages:
393
+ return {"error": "No messages found in request"}, 400
394
+
395
+ # Extract only the latest user message as the query (rely on session_id for context)
396
+ latest_user_query = ""
397
+ for msg in reversed(messages):
398
+ if msg.get('role', '') == 'user':
399
+ latest_user_query = msg.get('content', '')
400
+ break
401
+ if not latest_user_query:
402
+ return {"error": "No user message found in request"}, 400
403
+
404
+ # Add explicit instruction to reply in Chinese and be direct
405
+ query = f"请用英文思考,用中文回答以下问题,不要提及上下文或推理过程:{latest_user_query}"
406
+ print(f"Constructed Query for on-demand.io (relying on session_id for context, with Chinese instruction): {query}")
407
+
408
+ # Map the model ID to on-demand.io endpoint ID
409
+ model_mapping = {
410
+ "gpto3-mini": "predefined-openai-gpto3-mini",
411
+ "gpt-4o": "predefined-openai-gpt4o",
412
+ "gpt-4.1": "predefined-openai-gpt4.1",
413
+ "gpt-4.1-mini": "predefined-openai-gpt4.1-mini",
414
+ "gpt-4.1-nano": "predefined-openai-gpt4.1-nano",
415
+ "gpt-4o-mini": "predefined-openai-gpt4o-mini",
416
+ "deepseek-v3": "predefined-deepseek-v3",
417
+ "deepseek-r1": "predefined-deepseek-r1",
418
+ "claude-3.7-sonnet": "predefined-claude-3.7-sonnet",
419
+ "gemini-2.0-flash": "predefined-gemini-2.0-flash"
420
+ }
421
+ endpoint_id = model_mapping.get(model, "predefined-claude-3.7-sonnet") # Default to Claude if model not found
422
+
423
+ # Send query to OnDemand API
424
+ result = current_client.send_query(query, endpoint_id=endpoint_id, stream=stream)
425
+
426
+ if "error" in result:
427
+ return {"error": result["error"]}, 500
428
+
429
+ if stream:
430
+ def generate_stream():
431
+ for line in result["response"].iter_lines():
432
+ if line:
433
+ decoded_line = line.decode('utf-8')
434
+ if decoded_line.startswith("data:"):
435
+ json_str = decoded_line[len("data:"):]
436
+ if json_str == "[DONE]":
437
+ yield "data: [DONE]\n\n"
438
+ break
439
+ try:
440
+ event_data = json.loads(json_str)
441
+ if event_data.get("eventType", "") == "fulfillment":
442
+ content = event_data.get("answer", "")
443
+ stream_response = {
444
+ "id": f"chatcmpl-{int(time.time())}",
445
+ "object": "chat.completion.chunk",
446
+ "created": int(time.time()),
447
+ "model": model,
448
+ "choices": [
449
+ {
450
+ "delta": {"content": content},
451
+ "index": 0,
452
+ "finish_reason": None
453
+ }
454
+ ]
455
+ }
456
+ yield f"data: {json.dumps(stream_response)}\n\n"
457
+ except json.JSONDecodeError:
458
+ continue
459
+
460
+ return Response(stream_with_context(generate_stream()), content_type='text/event-stream')
461
+ else:
462
+ response = {
463
+ "id": f"chatcmpl-{int(time.time())}",
464
+ "object": "chat.completion",
465
+ "created": int(time.time()),
466
+ "model": model,
467
+ "choices": [
468
+ {
469
+ "message": {
470
+ "role": "assistant",
471
+ "content": result["content"]
472
+ },
473
+ "finish_reason": "stop",
474
+ "index": 0
475
+ }
476
+ ],
477
+ "usage": {
478
+ "prompt_tokens": 0, # Placeholder, can be updated if metrics are available
479
+ "completion_tokens": 0,
480
+ "total_tokens": 0
481
+ }
482
+ }
483
+ return response
484
+
485
+
486
+ if __name__ == "__main__":
487
+ # Get port from environment variable (Hugging Face Spaces uses PORT env var)
488
+ port = int(os.getenv("PORT", 7860))
489
+ print(f"Starting Flask app on port {port}")
490
+ # Run the Flask app with host 0.0.0.0 to be accessible in Docker
491
+ app.run(host='0.0.0.0', port=port, debug=False)
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python base image
2
+ FROM python:3.9-slim
3
+
4
+ # Set working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy requirements file
8
+ COPY requirements.txt .
9
+
10
+ # Install dependencies
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # Copy the application code
14
+ COPY . .
15
+
16
+ # Expose the port (will be overridden by PORT environment variable if set)
17
+ EXPOSE 7860
18
+
19
+ # Command to run the Flask app
20
+ CMD ["python", "2api.py"]
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ flask==2.3.2
2
+ requests==2.31.0