Spaces:
Paused
Paused
| import json | |
| import os | |
| import re | |
| import time | |
| import uuid | |
| import asyncio | |
| import threading | |
| from typing import Any, Dict, List, Optional, TypedDict, Union, Generator | |
| import requests | |
| from fastapi import FastAPI, HTTPException, Depends, Query | |
| from fastapi.responses import StreamingResponse | |
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | |
| from pydantic import BaseModel, Field | |
| # Yupp Account Management | |
| class YuppAccount(TypedDict): | |
| token: str | |
| is_valid: bool | |
| last_used: float | |
| error_count: int | |
| # Global variables | |
| VALID_CLIENT_KEYS: set = set() | |
| YUPP_ACCOUNTS: List[YuppAccount] = [] | |
| YUPP_MODELS: List[Dict[str, Any]] = [] | |
| account_rotation_lock = threading.Lock() | |
| MAX_ERROR_COUNT = 3 | |
| ERROR_COOLDOWN = 300 # 5 minutes cooldown for accounts with errors | |
| DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true" | |
| # Pydantic Models | |
| class ChatMessage(BaseModel): | |
| role: str | |
| content: Union[str, List[Dict[str, Any]]] | |
| reasoning_content: Optional[str] = None | |
| class ChatCompletionRequest(BaseModel): | |
| model: str | |
| messages: List[ChatMessage] | |
| stream: bool = True | |
| temperature: Optional[float] = None | |
| max_tokens: Optional[int] = None | |
| top_p: Optional[float] = None | |
| raw_response: bool = False # 保留用于调试 | |
| class ModelInfo(BaseModel): | |
| id: str | |
| object: str = "model" | |
| created: int | |
| owned_by: str | |
| class ModelList(BaseModel): | |
| object: str = "list" | |
| data: List[ModelInfo] | |
| class ChatCompletionChoice(BaseModel): | |
| message: ChatMessage | |
| index: int = 0 | |
| finish_reason: str = "stop" | |
| class ChatCompletionResponse(BaseModel): | |
| id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}") | |
| object: str = "chat.completion" | |
| created: int = Field(default_factory=lambda: int(time.time())) | |
| model: str | |
| choices: List[ChatCompletionChoice] | |
| usage: Dict[str, int] = Field( | |
| default_factory=lambda: { | |
| "prompt_tokens": 0, | |
| "completion_tokens": 0, | |
| "total_tokens": 0, | |
| } | |
| ) | |
| class StreamChoice(BaseModel): | |
| delta: Dict[str, Any] = Field(default_factory=dict) | |
| index: int = 0 | |
| finish_reason: Optional[str] = None | |
| class StreamResponse(BaseModel): | |
| id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}") | |
| object: str = "chat.completion.chunk" | |
| created: int = Field(default_factory=lambda: int(time.time())) | |
| model: str | |
| choices: List[StreamChoice] | |
| # FastAPI App | |
| app = FastAPI(title="Yupp.ai OpenAI API Adapter") | |
| security = HTTPBearer(auto_error=False) | |
| def log_debug(message: str): | |
| """Debug日志函数""" | |
| if DEBUG_MODE: | |
| print(f"[DEBUG] {message}") | |
| def load_client_api_keys(): | |
| """Load client API keys from client_api_keys.json""" | |
| global VALID_CLIENT_KEYS | |
| try: | |
| with open("client_api_keys.json", "r", encoding="utf-8") as f: | |
| keys = json.load(f) | |
| VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set() | |
| print(f"Successfully loaded {len(VALID_CLIENT_KEYS)} client API keys.") | |
| except FileNotFoundError: | |
| print("Error: client_api_keys.json not found. Client authentication will fail.") | |
| VALID_CLIENT_KEYS = set() | |
| except Exception as e: | |
| print(f"Error loading client_api_keys.json: {e}") | |
| VALID_CLIENT_KEYS = set() | |
| def load_yupp_accounts(): | |
| """Load Yupp accounts from yupp.json""" | |
| global YUPP_ACCOUNTS | |
| YUPP_ACCOUNTS = [] | |
| try: | |
| with open("yupp.json", "r", encoding="utf-8") as f: | |
| accounts = json.load(f) | |
| if not isinstance(accounts, list): | |
| print("Warning: yupp.json should contain a list of account objects.") | |
| return | |
| for acc in accounts: | |
| token = acc.get("token") | |
| if token: | |
| YUPP_ACCOUNTS.append({ | |
| "token": token, | |
| "is_valid": True, | |
| "last_used": 0, | |
| "error_count": 0 | |
| }) | |
| print(f"Successfully loaded {len(YUPP_ACCOUNTS)} Yupp accounts.") | |
| except FileNotFoundError: | |
| print("Error: yupp.json not found. API calls will fail.") | |
| except Exception as e: | |
| print(f"Error loading yupp.json: {e}") | |
| def load_yupp_models(): | |
| """Load Yupp models from model.json""" | |
| global YUPP_MODELS | |
| try: | |
| with open("model.json", "r", encoding="utf-8") as f: | |
| YUPP_MODELS = json.load(f) | |
| if not isinstance(YUPP_MODELS, list): | |
| YUPP_MODELS = [] | |
| print("Warning: model.json should contain a list of model objects.") | |
| return | |
| print(f"Successfully loaded {len(YUPP_MODELS)} models.") | |
| except FileNotFoundError: | |
| print("Error: model.json not found. Model list will be empty.") | |
| YUPP_MODELS = [] | |
| except Exception as e: | |
| print(f"Error loading model.json: {e}") | |
| YUPP_MODELS = [] | |
| def get_best_yupp_account() -> Optional[YuppAccount]: | |
| """Get the best available Yupp account using a smart selection algorithm.""" | |
| with account_rotation_lock: | |
| now = time.time() | |
| valid_accounts = [ | |
| acc for acc in YUPP_ACCOUNTS | |
| if acc["is_valid"] and ( | |
| acc["error_count"] < MAX_ERROR_COUNT or | |
| now - acc["last_used"] > ERROR_COOLDOWN | |
| ) | |
| ] | |
| if not valid_accounts: | |
| return None | |
| # Reset error count for accounts that have been in cooldown | |
| for acc in valid_accounts: | |
| if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN: | |
| acc["error_count"] = 0 | |
| # Sort by last used (oldest first) and error count (lowest first) | |
| valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"])) | |
| account = valid_accounts[0] | |
| account["last_used"] = now | |
| return account | |
| def format_messages_for_yupp(messages: List[ChatMessage]) -> str: | |
| """将多轮对话格式化为Yupp单轮对话格式""" | |
| formatted = [] | |
| # 处理系统消息 | |
| system_messages = [msg for msg in messages if msg.role == "system"] | |
| if system_messages: | |
| for sys_msg in system_messages: | |
| content = sys_msg.content if isinstance(sys_msg.content, str) else json.dumps(sys_msg.content) | |
| formatted.append(content) | |
| # 处理用户和助手消息 | |
| user_assistant_msgs = [msg for msg in messages if msg.role != "system"] | |
| for msg in user_assistant_msgs: | |
| role = "Human" if msg.role == "user" else "Assistant" | |
| content = msg.content if isinstance(msg.content, str) else json.dumps(msg.content) | |
| formatted.append(f"\n\n{role}: {content}") | |
| # 确保以Assistant:结尾 | |
| if not formatted or not formatted[-1].strip().startswith("Assistant:"): | |
| formatted.append("\n\nAssistant:") | |
| result = "".join(formatted) | |
| # 如果以\n\n开头,则删除 | |
| if result.startswith("\n\n"): | |
| result = result[2:] | |
| return result | |
| async def authenticate_client( | |
| auth: Optional[HTTPAuthorizationCredentials] = Depends(security), | |
| ): | |
| """Authenticate client based on API key in Authorization header""" | |
| if not VALID_CLIENT_KEYS: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Service unavailable: Client API keys not configured on server.", | |
| ) | |
| if not auth or not auth.credentials: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="API key required in Authorization header.", | |
| headers={"WWW-Authenticate": "Bearer"}, | |
| ) | |
| if auth.credentials not in VALID_CLIENT_KEYS: | |
| raise HTTPException(status_code=403, detail="Invalid client API key.") | |
| async def startup(): | |
| """应用启动时初始化配置""" | |
| print("Starting Yupp.ai OpenAI API Adapter server...") | |
| load_client_api_keys() | |
| load_yupp_accounts() | |
| load_yupp_models() | |
| print("Server initialization completed.") | |
| async def shutdown(): | |
| """应用关闭时清理资源""" | |
| print("Server shutdown completed.") | |
| def get_models_list_response() -> ModelList: | |
| """Helper to construct ModelList response from cached models.""" | |
| model_infos = [ | |
| ModelInfo( | |
| id=model.get("label", "unknown"), | |
| created=int(time.time()), | |
| owned_by=model.get("publisher", "unknown") | |
| ) | |
| for model in YUPP_MODELS | |
| ] | |
| return ModelList(data=model_infos) | |
| async def list_v1_models(_: None = Depends(authenticate_client)): | |
| """List available models - authenticated""" | |
| return get_models_list_response() | |
| async def list_models_no_auth(): | |
| """List available models without authentication - for client compatibility""" | |
| return get_models_list_response() | |
| async def toggle_debug(enable: bool = Query(None)): | |
| """切换调试模式""" | |
| global DEBUG_MODE | |
| if enable is not None: | |
| DEBUG_MODE = enable | |
| return {"debug_mode": DEBUG_MODE} | |
| def claim_yupp_reward(account: YuppAccount, reward_id: str): | |
| """同步领取Yupp奖励""" | |
| try: | |
| log_debug(f"Claiming reward {reward_id}...") | |
| url = "https://yupp.ai/api/trpc/reward.claim?batch=1" | |
| payload = {"0": {"json": {"rewardId": reward_id}}} | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0", | |
| "Content-Type": "application/json", | |
| "sec-fetch-site": "same-origin", | |
| "Cookie": f"__Secure-yupp.session-token={account['token']}", | |
| } | |
| response = requests.post(url, json=payload, headers=headers) | |
| response.raise_for_status() | |
| data = response.json() | |
| balance = data[0]["result"]["data"]["json"]["currentCreditBalance"] | |
| print(f"Reward claimed successfully. New balance: {balance}") | |
| return balance | |
| except Exception as e: | |
| print(f"Failed to claim reward {reward_id}. Error: {e}") | |
| return None | |
| def yupp_stream_generator(response_lines, model_id: str, account: YuppAccount) -> Generator[str, None, None]: | |
| """处理Yupp的流式响应并转换为OpenAI格式""" | |
| stream_id = f"chatcmpl-{uuid.uuid4().hex}" | |
| created_time = int(time.time()) | |
| # 发送初始角色 | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n" | |
| line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)") | |
| chunks = {} | |
| target_stream_id = None | |
| reward_info = None | |
| is_thinking = False | |
| thinking_content = "" | |
| normal_content = "" | |
| def extract_ref_id(ref): | |
| """从引用字符串中提取ID,例如从'$@123'提取'123'""" | |
| return ref[2:] if ref and isinstance(ref, str) and ref.startswith("$@") else None | |
| try: | |
| for line in response_lines: | |
| if not line: | |
| continue | |
| match = line_pattern.match(line) | |
| if not match: | |
| continue | |
| chunk_id, chunk_data = match.groups() | |
| chunk_id = chunk_id.decode() | |
| try: | |
| data = json.loads(chunk_data) if chunk_data != b"{}" else {} | |
| chunks[chunk_id] = data | |
| except json.JSONDecodeError: | |
| continue | |
| # 处理奖励信息 | |
| if chunk_id == "a": | |
| reward_info = data | |
| # 处理初始设置信息 | |
| elif chunk_id == "1": | |
| left_stream = data.get("leftStream", {}) | |
| right_stream = data.get("rightStream", {}) | |
| select_stream = [left_stream, right_stream] | |
| elif chunk_id == "e": | |
| for i, selection in enumerate(data.get("modelSelections", [])): | |
| if selection.get("selectionSource") == "USER_SELECTED": | |
| target_stream_id = extract_ref_id(select_stream[i].get("next")) | |
| break | |
| # 处理目标流内容 | |
| elif target_stream_id and chunk_id == target_stream_id: | |
| content = data.get("curr", "") | |
| if content: | |
| # 处理思考过程 | |
| if "<think>" in content: | |
| parts = content.split("<think>", 1) | |
| if parts[0]: # 思考标签前的内容 | |
| normal_content += parts[0] | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': parts[0]})]).json()}\n\n" | |
| is_thinking = True | |
| thinking_part = parts[1] | |
| if "</think>" in thinking_part: | |
| think_parts = thinking_part.split("</think>", 1) | |
| thinking_content += think_parts[0] | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': think_parts[0]})]).json()}\n\n" | |
| is_thinking = False | |
| if think_parts[1]: # 思考标签后的内容 | |
| normal_content += think_parts[1] | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': think_parts[1]})]).json()}\n\n" | |
| else: | |
| thinking_content += thinking_part | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': thinking_part})]).json()}\n\n" | |
| elif "</think>" in content and is_thinking: | |
| parts = content.split("</think>", 1) | |
| thinking_content += parts[0] | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': parts[0]})]).json()}\n\n" | |
| is_thinking = False | |
| if parts[1]: # 思考标签后的内容 | |
| normal_content += parts[1] | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': parts[1]})]).json()}\n\n" | |
| elif is_thinking: | |
| thinking_content += content | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'reasoning_content': content})]).json()}\n\n" | |
| else: | |
| normal_content += content | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'content': content})]).json()}\n\n" | |
| # 更新目标流ID | |
| target_stream_id = extract_ref_id(data.get("next")) | |
| except Exception as e: | |
| print(f"Stream processing error: {e}") | |
| yield f"data: {json.dumps({'error': str(e)})}\n\n" | |
| finally: | |
| # 发送完成信号 | |
| yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n" | |
| yield "data: [DONE]\n\n" | |
| # 领取奖励 | |
| if reward_info and "unclaimedRewardInfo" in reward_info: | |
| reward_id = reward_info["unclaimedRewardInfo"].get("rewardId") | |
| if reward_id: | |
| try: | |
| claim_yupp_reward(account, reward_id) | |
| except Exception as e: | |
| print(f"Failed to claim reward in background: {e}") | |
| def build_yupp_non_stream_response(response_lines, model_id: str, account: YuppAccount) -> ChatCompletionResponse: | |
| """构建非流式响应""" | |
| full_content = "" | |
| full_reasoning_content = "" | |
| reward_id = None | |
| for event in yupp_stream_generator(response_lines, model_id, account): | |
| if event.startswith("data:"): | |
| data_str = event[5:].strip() | |
| if data_str == "[DONE]": | |
| break | |
| try: | |
| data = json.loads(data_str) | |
| if "error" in data: | |
| raise HTTPException(status_code=500, detail=data["error"]["message"]) | |
| delta = data.get("choices", [{}])[0].get("delta", {}) | |
| if "content" in delta: | |
| full_content += delta["content"] | |
| if "reasoning_content" in delta: | |
| full_reasoning_content += delta["reasoning_content"] | |
| except json.JSONDecodeError: | |
| continue | |
| # 构建完整响应 | |
| return ChatCompletionResponse( | |
| model=model_id, | |
| choices=[ | |
| ChatCompletionChoice( | |
| message=ChatMessage( | |
| role="assistant", | |
| content=full_content, | |
| reasoning_content=full_reasoning_content if full_reasoning_content else None, | |
| ) | |
| ) | |
| ], | |
| ) | |
| async def chat_completions( | |
| request: ChatCompletionRequest, _: None = Depends(authenticate_client) | |
| ): | |
| """使用Yupp.ai创建聊天完成""" | |
| # 查找模型 | |
| model_info = next((m for m in YUPP_MODELS if m.get("label") == request.model), None) | |
| if not model_info: | |
| raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.") | |
| model_name = model_info.get("name") | |
| if not model_name: | |
| raise HTTPException(status_code=404, detail=f"Model '{request.model}' has no 'name' field.") | |
| if not request.messages: | |
| raise HTTPException(status_code=400, detail="No messages provided in the request.") | |
| log_debug(f"Processing request for model: {request.model} (Yupp name: {model_name})") | |
| # 格式化消息 | |
| question = format_messages_for_yupp(request.messages) | |
| log_debug(f"Formatted question: {question[:100]}...") | |
| # 尝试所有账户 | |
| for attempt in range(len(YUPP_ACCOUNTS)): | |
| account = get_best_yupp_account() | |
| if not account: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="No valid Yupp.ai accounts available." | |
| ) | |
| try: | |
| # 构建请求 | |
| url_uuid = str(uuid.uuid4()) | |
| url = f"https://yupp.ai/chat/{url_uuid}" | |
| payload = [ | |
| url_uuid, | |
| str(uuid.uuid4()), | |
| question, | |
| "$undefined", | |
| "$undefined", | |
| [], | |
| "$undefined", | |
| [{"modelName": model_name, "promptModifierId": "$undefined"}], | |
| "text", | |
| False, | |
| ] | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0", | |
| "Accept": "text/x-component", | |
| "Accept-Encoding": "gzip, deflate, br, zstd", | |
| "Content-Type": "application/json", | |
| "next-action": "7f48888536e2f0c0163640837db291777c39cc40c3", | |
| "sec-fetch-site": "same-origin", | |
| "Cookie": f"__Secure-yupp.session-token={account['token']}", | |
| } | |
| log_debug(f"Sending request to Yupp.ai with account token ending in ...{account['token'][-4:]}") | |
| # 发送请求 | |
| response = requests.post( | |
| url, | |
| data=json.dumps(payload), | |
| headers=headers, | |
| stream=True | |
| ) | |
| response.raise_for_status() | |
| # 处理响应 | |
| if request.stream: | |
| log_debug("Returning processed response stream") | |
| return StreamingResponse( | |
| yupp_stream_generator(response.iter_lines(), request.model, account), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| "X-Accel-Buffering": "no", | |
| }, | |
| ) | |
| else: | |
| log_debug("Building non-stream response") | |
| return build_yupp_non_stream_response(response.iter_lines(), model_name, account) | |
| except requests.exceptions.HTTPError as e: | |
| status_code = e.response.status_code | |
| error_detail = e.response.text | |
| print(f"Yupp.ai API error ({status_code}): {error_detail}") | |
| with account_rotation_lock: | |
| if status_code in [401, 403]: | |
| account["is_valid"] = False | |
| print(f"Account ...{account['token'][-4:]} marked as invalid due to auth error.") | |
| elif status_code in [429, 500, 502, 503, 504]: | |
| account["error_count"] += 1 | |
| print(f"Account ...{account['token'][-4:]} error count: {account['error_count']}") | |
| else: | |
| # 客户端错误,不尝试使用其他账户 | |
| raise HTTPException(status_code=status_code, detail=error_detail) | |
| except Exception as e: | |
| print(f"Request error: {e}") | |
| with account_rotation_lock: | |
| account["error_count"] += 1 | |
| # 所有尝试都失败 | |
| raise HTTPException(status_code=503, detail="All attempts to contact Yupp.ai API failed.") | |
| async def error_stream_generator(error_detail: str, status_code: int): | |
| """生成错误流响应""" | |
| yield f'data: {json.dumps({"error": {"message": error_detail, "type": "yupp_api_error", "code": status_code}})}\n\n' | |
| yield "data: [DONE]\n\n" | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # 设置环境变量以启用调试模式 | |
| if os.environ.get("DEBUG_MODE", "").lower() == "true": | |
| DEBUG_MODE = True | |
| print("Debug mode enabled via environment variable") | |
| if not os.path.exists("yupp.json"): | |
| print("Warning: yupp.json not found. Creating a dummy file.") | |
| dummy_data = [ | |
| { | |
| "token": "your_yupp_session_token_here", | |
| } | |
| ] | |
| with open("yupp.json", "w", encoding="utf-8") as f: | |
| json.dump(dummy_data, f, indent=4) | |
| print("Created dummy yupp.json. Please replace with valid Yupp.ai data.") | |
| if not os.path.exists("client_api_keys.json"): | |
| print("Warning: client_api_keys.json not found. Creating a dummy file.") | |
| dummy_key = f"sk-dummy-{uuid.uuid4().hex}" | |
| with open("client_api_keys.json", "w", encoding="utf-8") as f: | |
| json.dump([dummy_key], f, indent=2) | |
| print(f"Created dummy client_api_keys.json with key: {dummy_key}") | |
| if not os.path.exists("model.json"): | |
| print("Warning: model.json not found. Creating a dummy file.") | |
| dummy_models = [ | |
| { | |
| "id": "claude-3.7-sonnet:thinking", | |
| "name": "anthropic/claude-3.7-sonnet:thinking<>OPR", | |
| "label": "Claude 3.7 Sonnet (Thinking) (OpenRouter)", | |
| "publisher": "Anthropic", | |
| "family": "Claude" | |
| } | |
| ] | |
| with open("model.json", "w", encoding="utf-8") as f: | |
| json.dump(dummy_models, f, indent=4) | |
| print("Created dummy model.json.") | |
| load_client_api_keys() | |
| load_yupp_accounts() | |
| load_yupp_models() | |
| print("\n--- Yupp.ai OpenAI API Adapter ---") | |
| print(f"Debug Mode: {DEBUG_MODE}") | |
| print("Endpoints:") | |
| print(" GET /v1/models (Client API Key Auth)") | |
| print(" GET /models (No Auth)") | |
| print(" POST /v1/chat/completions (Client API Key Auth)") | |
| print(" GET /debug?enable=[true|false] (Toggle Debug Mode)") | |
| print(f"\nClient API Keys: {len(VALID_CLIENT_KEYS)}") | |
| if YUPP_ACCOUNTS: | |
| print(f"Yupp.ai Accounts: {len(YUPP_ACCOUNTS)}") | |
| else: | |
| print("Yupp.ai Accounts: None loaded. Check yupp.json.") | |
| if YUPP_MODELS: | |
| models = sorted([m.get("label", m.get("id", "unknown")) for m in YUPP_MODELS]) | |
| print(f"Yupp.ai Models: {len(YUPP_MODELS)}") | |
| print(f"Available models: {', '.join(models[:5])}{'...' if len(models) > 5 else ''}") | |
| else: | |
| print("Yupp.ai Models: None loaded. Check model.json.") | |
| print("------------------------------------") | |
| uvicorn.run(app, host="0.0.0.0", port=8000) |