|
|
import json |
|
|
import os |
|
|
import time |
|
|
import uuid |
|
|
import threading |
|
|
from typing import Any, Dict, List, Optional, TypedDict, Union |
|
|
from urllib.parse import urlparse |
|
|
|
|
|
import requests |
|
|
import websocket |
|
|
from fastapi import FastAPI, HTTPException, Depends, Query |
|
|
from fastapi.responses import StreamingResponse |
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials |
|
|
from pydantic import BaseModel, Field |
|
|
|
|
|
from getCaptcha import getCaptcha, getTaskId |
|
|
|
|
|
|
|
|
|
|
|
class TenbinAccount(TypedDict): |
|
|
session_id: str |
|
|
is_valid: bool |
|
|
last_used: float |
|
|
error_count: int |
|
|
|
|
|
|
|
|
|
|
|
VALID_CLIENT_KEYS: set = set() |
|
|
TENBIN_ACCOUNTS: List[TenbinAccount] = [] |
|
|
TENBIN_MODELS: Dict[str, str] = {} |
|
|
account_rotation_lock = threading.Lock() |
|
|
MAX_ERROR_COUNT = 3 |
|
|
ERROR_COOLDOWN = 300 |
|
|
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true" |
|
|
REQUEST_TIMEOUT = 120.0 |
|
|
HTTP_PROXY = os.environ.get("HTTP_PROXY") |
|
|
PROXIES = {"http": HTTP_PROXY, "https": HTTP_PROXY} if HTTP_PROXY else None |
|
|
WEBSOCKET_PROXY_CONFIG = {} |
|
|
|
|
|
if HTTP_PROXY: |
|
|
print(f"Using HTTP/HTTPS proxy: {HTTP_PROXY}") |
|
|
parsed_proxy_url = urlparse(HTTP_PROXY) |
|
|
WEBSOCKET_PROXY_CONFIG = { |
|
|
"http_proxy_host": parsed_proxy_url.hostname, |
|
|
"http_proxy_port": parsed_proxy_url.port, |
|
|
} |
|
|
if parsed_proxy_url.username and parsed_proxy_url.password: |
|
|
WEBSOCKET_PROXY_CONFIG["http_proxy_auth"] = ( |
|
|
parsed_proxy_url.username, |
|
|
parsed_proxy_url.password, |
|
|
) |
|
|
print(f"Using WebSocket proxy config: {WEBSOCKET_PROXY_CONFIG}") |
|
|
|
|
|
|
|
|
|
|
|
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 = "tenbin" |
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(title="Tenbin 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 environment variable (comma-separated) or client_api_keys.json""" |
|
|
global VALID_CLIENT_KEYS |
|
|
keys_str = os.environ.get("API_KEYS") |
|
|
try: |
|
|
if keys_str: |
|
|
print("Loading client API keys from API_KEYS environment variable.") |
|
|
keys = [key.strip() for key in keys_str.split(',')] |
|
|
else: |
|
|
print("Loading client API keys from file: client_api_keys.json") |
|
|
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 and API_KEYS not set. Client authentication will fail.") |
|
|
VALID_CLIENT_KEYS = set() |
|
|
except Exception as e: |
|
|
print(f"Error loading client API keys: {e}") |
|
|
VALID_CLIENT_KEYS = set() |
|
|
|
|
|
|
|
|
def load_tenbin_accounts(): |
|
|
"""Load Tenbin accounts from environment variable (comma-separated session_ids) or tenbin.json""" |
|
|
global TENBIN_ACCOUNTS |
|
|
TENBIN_ACCOUNTS = [] |
|
|
session_ids_str = os.environ.get("SESSION_IDS") |
|
|
try: |
|
|
accounts_to_process = [] |
|
|
if session_ids_str: |
|
|
print("Loading Tenbin accounts from SESSION_IDS environment variable.") |
|
|
session_ids = [sid.strip() for sid in session_ids_str.split(',')] |
|
|
accounts_to_process = [{"session_id": sid} for sid in session_ids] |
|
|
else: |
|
|
print("Loading Tenbin accounts from file: tenbin.json") |
|
|
with open("tenbin.json", "r", encoding="utf-8") as f: |
|
|
accounts_to_process = json.load(f) |
|
|
|
|
|
if not isinstance(accounts_to_process, list): |
|
|
print("Warning: Account data should be a list of objects.") |
|
|
return |
|
|
|
|
|
for acc in accounts_to_process: |
|
|
session_id = acc.get("session_id") |
|
|
if session_id: |
|
|
TENBIN_ACCOUNTS.append({ |
|
|
"session_id": session_id, |
|
|
"is_valid": True, |
|
|
"last_used": 0, |
|
|
"error_count": 0 |
|
|
}) |
|
|
print(f"Successfully loaded {len(TENBIN_ACCOUNTS)} Tenbin accounts.") |
|
|
except FileNotFoundError: |
|
|
print("Error: tenbin.json not found and SESSION_IDS not set. API calls will fail.") |
|
|
except Exception as e: |
|
|
print(f"Error loading tenbin.json: {e}") |
|
|
|
|
|
|
|
|
def load_tenbin_models(): |
|
|
"""Load Tenbin models from models.json""" |
|
|
global TENBIN_MODELS |
|
|
try: |
|
|
with open("models.json", "r", encoding="utf-8") as f: |
|
|
models_data = json.load(f) |
|
|
if isinstance(models_data, dict): |
|
|
TENBIN_MODELS = models_data |
|
|
print(f"Successfully loaded {len(TENBIN_MODELS)} models.") |
|
|
else: |
|
|
print("Warning: models.json should contain a dictionary of model mappings.") |
|
|
TENBIN_MODELS = {} |
|
|
except FileNotFoundError: |
|
|
print("Error: models.json not found. Model list will be empty.") |
|
|
TENBIN_MODELS = {} |
|
|
except Exception as e: |
|
|
print(f"Error loading models.json: {e}") |
|
|
TENBIN_MODELS = {} |
|
|
|
|
|
|
|
|
def get_best_tenbin_account() -> Optional[TenbinAccount]: |
|
|
"""Get the best available Tenbin account using a smart selection algorithm.""" |
|
|
with account_rotation_lock: |
|
|
now = time.time() |
|
|
valid_accounts = [ |
|
|
acc for acc in TENBIN_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 |
|
|
|
|
|
|
|
|
for acc in valid_accounts: |
|
|
if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN: |
|
|
acc["error_count"] = 0 |
|
|
|
|
|
|
|
|
valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"])) |
|
|
account = valid_accounts[0] |
|
|
account["last_used"] = now |
|
|
return account |
|
|
|
|
|
|
|
|
def build_tenbin_prompt(messages: List[ChatMessage]) -> str: |
|
|
"""将 OpenAI 格式的消息列表转换为 Tenbin 格式的单个字符串""" |
|
|
prompt = "" |
|
|
for msg in messages: |
|
|
role = msg.role |
|
|
content = msg.content |
|
|
if isinstance(content, list): |
|
|
|
|
|
content = " ".join([ |
|
|
item.get("text", "") |
|
|
for item in content |
|
|
if item.get("type") == "text" |
|
|
]) |
|
|
|
|
|
|
|
|
if role == "system": |
|
|
|
|
|
prompt += f"\n\nHuman: <system>{content}</system>" |
|
|
elif role == "user": |
|
|
prompt += f"\n\nHuman: {content}" |
|
|
elif role == "assistant": |
|
|
prompt += f"\n\nAssistant: {content}" |
|
|
|
|
|
|
|
|
|
|
|
prompt += "\n\nAssistant:" |
|
|
return prompt |
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
@app.on_event("startup") |
|
|
async def startup(): |
|
|
"""应用启动时初始化配置""" |
|
|
print("Starting Tenbin OpenAI API Adapter server...") |
|
|
load_client_api_keys() |
|
|
load_tenbin_accounts() |
|
|
load_tenbin_models() |
|
|
print("Server initialization completed.") |
|
|
|
|
|
|
|
|
def get_models_list_response() -> ModelList: |
|
|
"""Helper to construct ModelList response from cached models.""" |
|
|
model_infos = [ |
|
|
ModelInfo( |
|
|
id=model_id, |
|
|
created=int(time.time()), |
|
|
owned_by="tenbin" |
|
|
) |
|
|
for model_id in TENBIN_MODELS.keys() |
|
|
] |
|
|
return ModelList(data=model_infos) |
|
|
|
|
|
|
|
|
@app.get("/v1/models", response_model=ModelList) |
|
|
async def list_v1_models(_: None = Depends(authenticate_client)): |
|
|
"""List available models - authenticated""" |
|
|
return get_models_list_response() |
|
|
|
|
|
|
|
|
@app.get("/models", response_model=ModelList) |
|
|
async def list_models_no_auth(): |
|
|
"""List available models without authentication - for client compatibility""" |
|
|
return get_models_list_response() |
|
|
|
|
|
|
|
|
@app.get("/debug") |
|
|
async def toggle_debug(enable: bool = Query(None)): |
|
|
"""切换调试模式""" |
|
|
global DEBUG_MODE |
|
|
if enable is not None: |
|
|
DEBUG_MODE = enable |
|
|
return {"debug_mode": DEBUG_MODE} |
|
|
|
|
|
|
|
|
@app.post("/v1/chat/completions") |
|
|
async def chat_completions( |
|
|
request: ChatCompletionRequest, _: None = Depends(authenticate_client) |
|
|
): |
|
|
"""创建聊天完成 - 使用 Tenbin API""" |
|
|
|
|
|
if request.model not in TENBIN_MODELS: |
|
|
raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found.") |
|
|
|
|
|
|
|
|
internal_model_id = TENBIN_MODELS[request.model] |
|
|
|
|
|
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} (internal ID: {internal_model_id})") |
|
|
|
|
|
|
|
|
prompt = build_tenbin_prompt(request.messages) |
|
|
log_debug(f"Built prompt with length: {len(prompt)}") |
|
|
|
|
|
|
|
|
for attempt in range(len(TENBIN_ACCOUNTS)): |
|
|
account = get_best_tenbin_account() |
|
|
if not account: |
|
|
raise HTTPException( |
|
|
status_code=503, |
|
|
detail="No valid Tenbin accounts available." |
|
|
) |
|
|
|
|
|
session_id = account["session_id"] |
|
|
log_debug(f"Using account with session_id ending in ...{session_id[-4:]}") |
|
|
|
|
|
try: |
|
|
|
|
|
execution_token = get_tenbin_execution_token(internal_model_id, session_id) |
|
|
|
|
|
if request.stream: |
|
|
log_debug("Returning stream response") |
|
|
return StreamingResponse( |
|
|
tenbin_stream_generator(request.model, prompt, session_id, execution_token), |
|
|
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_tenbin_non_stream_response(request.model, prompt, session_id, execution_token) |
|
|
|
|
|
except Exception as e: |
|
|
error_detail = str(e) |
|
|
log_debug(f"Tenbin API error: {error_detail}") |
|
|
|
|
|
with account_rotation_lock: |
|
|
|
|
|
account["error_count"] += 1 |
|
|
log_debug(f"Account ...{session_id[-4:]} error count: {account['error_count']}") |
|
|
|
|
|
|
|
|
if "authentication" in error_detail.lower() or "unauthorized" in error_detail.lower(): |
|
|
account["is_valid"] = False |
|
|
log_debug(f"Account ...{session_id[-4:]} marked as invalid due to auth error.") |
|
|
|
|
|
|
|
|
if request.stream: |
|
|
return StreamingResponse( |
|
|
error_stream_generator("All attempts to contact Tenbin API failed.", 503), |
|
|
media_type="text/event-stream", |
|
|
status_code=503, |
|
|
) |
|
|
else: |
|
|
raise HTTPException(status_code=503, detail="All attempts to contact Tenbin API failed.") |
|
|
|
|
|
|
|
|
def get_tenbin_execution_token(model: str, session_id: str) -> str: |
|
|
"""获取 Tenbin 执行令牌""" |
|
|
try: |
|
|
task_id = getTaskId() |
|
|
captcha = getCaptcha(task_id) |
|
|
url = "https://graphql.tenbin.ai/graphql" |
|
|
|
|
|
payload = { |
|
|
"operationName": "IssueExecutionTokensMultiple", |
|
|
"variables": { |
|
|
"turnstileToken": captcha, |
|
|
"models": [model], |
|
|
}, |
|
|
"query": "query IssueExecutionTokensMultiple($turnstileToken: String!, $models: [ChatModel!]!) {\n executionTokens: issueExecutionTokensMultiple(\n turnstileToken: $turnstileToken\n models: $models\n )\n}", |
|
|
} |
|
|
|
|
|
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", |
|
|
"Accept-Encoding": "gzip, deflate, br, zstd", |
|
|
"Content-Type": "application/json", |
|
|
"Cookie": f"sessionId={session_id}", |
|
|
} |
|
|
|
|
|
log_debug(f"Getting execution token for model: {model}") |
|
|
response = requests.post( |
|
|
url, |
|
|
data=json.dumps(payload), |
|
|
headers=headers, |
|
|
timeout=REQUEST_TIMEOUT, |
|
|
proxies=PROXIES |
|
|
) |
|
|
response.raise_for_status() |
|
|
|
|
|
execution_token = response.json()["data"]["executionTokens"][0] |
|
|
log_debug(f"Got execution token: {execution_token[:10]}...") |
|
|
return execution_token |
|
|
except Exception as e: |
|
|
log_debug(f"Error getting execution token: {e}") |
|
|
raise |
|
|
|
|
|
|
|
|
def tenbin_stream_generator(model: str, prompt: str, session_id: str, execution_token: str): |
|
|
"""Tenbin WebSocket 流式响应生成器""" |
|
|
stream_id = f"chatcmpl-{uuid.uuid4().hex}" |
|
|
created_time = int(time.time()) |
|
|
|
|
|
|
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n" |
|
|
|
|
|
|
|
|
url = "wss://graphql.tenbin.ai/graphql" |
|
|
headers = { |
|
|
"Host": "graphql.tenbin.ai", |
|
|
"Connection": "Upgrade", |
|
|
"Pragma": "no-cache", |
|
|
"Cache-Control": "no-cache", |
|
|
"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", |
|
|
"Upgrade": "websocket", |
|
|
"Origin": "https://tenbin.ai", |
|
|
"Sec-WebSocket-Version": "13", |
|
|
"Accept-Encoding": "gzip, deflate, br, zstd", |
|
|
"Accept-Language": "zh-CN,zh;q=0.9", |
|
|
"Cookie": f"sessionId={session_id}", |
|
|
"Sec-WebSocket-Key": "I/tTy5psJkboWYQfCypjVA==", |
|
|
"Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", |
|
|
"Sec-WebSocket-Protocol": "graphql-transport-ws", |
|
|
} |
|
|
|
|
|
ws = None |
|
|
try: |
|
|
log_debug("Connecting to WebSocket...") |
|
|
ws = websocket.create_connection( |
|
|
url, |
|
|
header=headers, |
|
|
**WEBSOCKET_PROXY_CONFIG |
|
|
) |
|
|
ws.send(json.dumps({"type": "connection_init"})) |
|
|
init_response = ws.recv() |
|
|
log_debug(f"WebSocket init response: {init_response}") |
|
|
|
|
|
|
|
|
payload = { |
|
|
"id": str(uuid.uuid4()), |
|
|
"type": "subscribe", |
|
|
"payload": { |
|
|
"variables": { |
|
|
"prompt": prompt, |
|
|
"executionToken": execution_token, |
|
|
"stateToken": "", |
|
|
}, |
|
|
"extensions": {}, |
|
|
"operationName": "StartConversation", |
|
|
"query": "subscription StartConversation($executionToken: String!, $itemId: String, $itemDraftId: String, $systemPrompt: String, $prompt: String, $stateToken: String, $variables: [ConversationVariableInput!], $itemCallOption: ItemCallOption, $fileKey: String, $fileUploadIds: [String!], $selectedToolsByUser: [ToolType!]) {\n startConversation(\n executionToken: $executionToken\n itemId: $itemId\n itemDraftId: $itemDraftId\n systemPrompt: $systemPrompt\n prompt: $prompt\n stateToken: $stateToken\n variables: $variables\n itemCallOption: $itemCallOption\n fileKey: $fileKey\n fileUploadIds: $fileUploadIds\n selectedToolsByUser: $selectedToolsByUser\n ) {\n ...DeltaConversation\n __typename\n }\n}\n\nfragment DeltaConversation on AIConversationStreamResult {\n seq\n deltaToken\n isFinished\n newStateToken\n error\n fileUploadIds\n toolResult {\n id\n title\n url\n faviconUrl\n summary\n __typename\n }\n action\n activity\n toolError\n __typename\n}", |
|
|
}, |
|
|
} |
|
|
|
|
|
log_debug("Sending subscription request...") |
|
|
ws.send(json.dumps(payload)) |
|
|
|
|
|
|
|
|
accumulated_thinking = "" |
|
|
thinking_mode = False |
|
|
thinking_separator = "\n\n---\n\n" |
|
|
is_thinking_model = model == "Claude-3.7-Sonnet-Extended" |
|
|
|
|
|
while True: |
|
|
try: |
|
|
msg = ws.recv() |
|
|
log_debug(f"Received message: {msg[:100]}..." if len(msg) > 100 else msg) |
|
|
|
|
|
if msg.endswith('"type":"complete"}'): |
|
|
log_debug("Received complete message") |
|
|
break |
|
|
|
|
|
try: |
|
|
data = json.loads(msg) |
|
|
if data.get("type") != "next": |
|
|
continue |
|
|
|
|
|
payload_data = data.get("payload", {}).get("data", {}) |
|
|
conversation = payload_data.get("startConversation", {}) |
|
|
|
|
|
delta_token = conversation.get("deltaToken", "") |
|
|
is_finished = conversation.get("isFinished", False) |
|
|
|
|
|
if delta_token: |
|
|
if is_thinking_model: |
|
|
|
|
|
if thinking_separator in accumulated_thinking + delta_token: |
|
|
|
|
|
if not thinking_mode: |
|
|
|
|
|
parts = (accumulated_thinking + delta_token).split(thinking_separator, 1) |
|
|
thinking_content = parts[0] |
|
|
answer_content = parts[1] if len(parts) > 1 else "" |
|
|
|
|
|
if thinking_content: |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': thinking_content})]).json()}\n\n" |
|
|
|
|
|
if answer_content: |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': answer_content})]).json()}\n\n" |
|
|
|
|
|
thinking_mode = True |
|
|
accumulated_thinking = "" |
|
|
else: |
|
|
|
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n" |
|
|
else: |
|
|
|
|
|
if thinking_mode: |
|
|
|
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n" |
|
|
else: |
|
|
|
|
|
accumulated_thinking += delta_token |
|
|
else: |
|
|
|
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'content': delta_token})]).json()}\n\n" |
|
|
|
|
|
if is_finished: |
|
|
|
|
|
if is_thinking_model and not thinking_mode and accumulated_thinking: |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={'reasoning_content': accumulated_thinking})]).json()}\n\n" |
|
|
|
|
|
|
|
|
log_debug("Stream finished") |
|
|
yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n" |
|
|
yield "data: [DONE]\n\n" |
|
|
break |
|
|
|
|
|
except json.JSONDecodeError as e: |
|
|
log_debug(f"JSON decode error: {e}") |
|
|
continue |
|
|
|
|
|
except websocket.WebSocketConnectionClosedException: |
|
|
log_debug("WebSocket connection closed") |
|
|
break |
|
|
|
|
|
except Exception as e: |
|
|
log_debug(f"Error processing message: {e}") |
|
|
yield f"data: {json.dumps({'error': str(e)})}\n\n" |
|
|
break |
|
|
|
|
|
except Exception as e: |
|
|
log_debug(f"WebSocket error: {e}") |
|
|
yield f"data: {json.dumps({'error': str(e)})}\n\n" |
|
|
yield "data: [DONE]\n\n" |
|
|
|
|
|
finally: |
|
|
if ws: |
|
|
try: |
|
|
ws.close() |
|
|
log_debug("WebSocket connection closed") |
|
|
except: |
|
|
pass |
|
|
|
|
|
|
|
|
def build_tenbin_non_stream_response(model: str, prompt: str, session_id: str, execution_token: str) -> ChatCompletionResponse: |
|
|
"""构建非流式响应""" |
|
|
full_content = "" |
|
|
full_reasoning_content = None |
|
|
|
|
|
|
|
|
for chunk in tenbin_stream_generator(model, prompt, session_id, execution_token): |
|
|
if not chunk.startswith("data: ") or chunk.strip() == "data: [DONE]": |
|
|
continue |
|
|
|
|
|
try: |
|
|
data = json.loads(chunk[6:]) |
|
|
if "choices" not in data: |
|
|
continue |
|
|
|
|
|
delta = data["choices"][0].get("delta", {}) |
|
|
|
|
|
if "content" in delta and delta["content"]: |
|
|
full_content += delta["content"] |
|
|
|
|
|
if "reasoning_content" in delta and delta["reasoning_content"]: |
|
|
if full_reasoning_content is None: |
|
|
full_reasoning_content = "" |
|
|
full_reasoning_content += delta["reasoning_content"] |
|
|
|
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
|
|
|
return ChatCompletionResponse( |
|
|
model=model, |
|
|
choices=[ |
|
|
ChatCompletionChoice( |
|
|
message=ChatMessage( |
|
|
role="assistant", |
|
|
content=full_content, |
|
|
reasoning_content=full_reasoning_content, |
|
|
) |
|
|
) |
|
|
], |
|
|
) |
|
|
|
|
|
|
|
|
async def error_stream_generator(error_detail: str, status_code: int): |
|
|
"""Generate error stream response""" |
|
|
yield f'data: {json.dumps({"error": {"message": error_detail, "type": "tenbin_api_error", "code": status_code}})}\n\n' |
|
|
yield "data: [DONE]\n\n" |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import uvicorn |
|
|
|
|
|
|
|
|
port = int(os.environ.get("PORT", 8000)) |
|
|
|
|
|
|
|
|
if os.environ.get("DEBUG_MODE", "").lower() == "true": |
|
|
DEBUG_MODE = True |
|
|
print("Debug mode enabled via environment variable") |
|
|
|
|
|
if not os.path.exists("tenbin.json") and not os.environ.get("SESSION_IDS"): |
|
|
print("Warning: tenbin.json not found and SESSION_IDS not set. Creating a dummy file.") |
|
|
dummy_data = [ |
|
|
{ |
|
|
"session_id": "your_session_id_here", |
|
|
} |
|
|
] |
|
|
with open("tenbin.json", "w", encoding="utf-8") as f: |
|
|
json.dump(dummy_data, f, indent=4) |
|
|
print("Created dummy tenbin.json. Please replace with valid Tenbin data or set SESSION_IDS secret.") |
|
|
|
|
|
if not os.path.exists("client_api_keys.json") and not os.environ.get("API_KEYS"): |
|
|
print("Warning: client_api_keys.json not found and API_KEYS not set. 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}. Or set API_KEYS secret.") |
|
|
|
|
|
if not os.path.exists("models.json"): |
|
|
print("Warning: models.json not found. Creating a dummy file.") |
|
|
dummy_models = { |
|
|
"claude-3.7-sonnet": "AnthropicClaude37Sonnet", |
|
|
"claude-3.7-sonnet-extended": "AnthropicClaude37SonnetExtended" |
|
|
} |
|
|
with open("models.json", "w", encoding="utf-8") as f: |
|
|
json.dump(dummy_models, f, indent=4) |
|
|
print("Created dummy models.json.") |
|
|
|
|
|
load_client_api_keys() |
|
|
load_tenbin_accounts() |
|
|
load_tenbin_models() |
|
|
|
|
|
print("\n--- Tenbin 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 TENBIN_ACCOUNTS: |
|
|
print(f"Tenbin Accounts: {len(TENBIN_ACCOUNTS)}") |
|
|
else: |
|
|
print("Tenbin Accounts: None loaded. Check tenbin.json.") |
|
|
if TENBIN_MODELS: |
|
|
models = sorted(list(TENBIN_MODELS.keys())) |
|
|
print(f"Tenbin Models: {len(TENBIN_MODELS)}") |
|
|
print(f"Available models: {', '.join(models[:5])}{'...' if len(models) > 5 else ''}") |
|
|
else: |
|
|
print("Tenbin Models: None loaded. Check models.json.") |
|
|
print("------------------------------------") |
|
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=port) |