hins111 commited on
Commit
9d831fa
·
verified ·
1 Parent(s): 64daa20

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +211 -0
main.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===================================================================
2
+ # main.py (已修改以适配 Hugging Face Secrets)
3
+ # ===================================================================
4
+
5
+ import httpx
6
+ import time
7
+ import json
8
+ import os
9
+ from fastapi import FastAPI, Request, HTTPException, Depends
10
+ from fastapi.responses import JSONResponse, StreamingResponse
11
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
12
+ from pydantic import BaseModel, Field
13
+ from typing import List, Optional, Dict, Any, Union
14
+
15
+ # --- Pydantic 模型定义 ---
16
+ class ChatMessage(BaseModel):
17
+ role: str
18
+ content: str
19
+
20
+ class ChatCompletionRequest(BaseModel):
21
+ model: str
22
+ messages: List[ChatMessage]
23
+ stream: bool = False
24
+ temperature: Optional[float] = None
25
+ max_tokens: Optional[int] = None
26
+ top_p: Optional[float] = None
27
+
28
+ # --- 全局变量 ---
29
+ config: Dict[str, Any] = {}
30
+ app = FastAPI(title="Kodu2API Adapter", description="将 Kodu AI API 转换为 OpenAI 格式")
31
+ security = HTTPBearer()
32
+
33
+ # --- 核心函数 (已修改) ---
34
+
35
+ def load_config():
36
+ """
37
+ 从环境变量加载配置 (适配 Hugging Face Secrets).
38
+ """
39
+ global config
40
+ print("Loading configuration from environment variables...")
41
+
42
+ try:
43
+ # 必需的 Secret: Kodu Refresh Token
44
+ kodu_refresh_token = os.environ.get("KODU_REFRESH_TOKEN")
45
+ if not kodu_refresh_token:
46
+ raise ValueError("Secret 'KODU_REFRESH_TOKEN' is required but not found.")
47
+
48
+ # 必需的 Secret: 你的服务密码 (API Keys)
49
+ service_api_keys_str = os.environ.get("SERVICE_API_KEYS")
50
+ if not service_api_keys_str:
51
+ raise ValueError("Secret 'SERVICE_API_KEYS' is required but not found.")
52
+ service_api_keys = json.loads(service_api_keys_str)
53
+
54
+ # 可选的 Secret: Kodu 模型映射
55
+ kodu_models_str = os.environ.get("KODU_MODELS")
56
+ kodu_models = json.loads(kodu_models_str) if kodu_models_str else {}
57
+
58
+ # 可选的 Secret: 代理 URL
59
+ proxy_url = os.environ.get("PROXY_URL") or None
60
+
61
+ # 可选的 Secret: Base URL
62
+ base_url = os.environ.get("BASE_URL", "https://api.kodu.ai")
63
+
64
+ # 组合成 config 字典
65
+ config = {
66
+ "kodu_refresh_token": kodu_refresh_token,
67
+ "kodu_access_token": "", # 初始为空,后续会自动刷新
68
+ "kodu_models": kodu_models,
69
+ "service_api_keys": service_api_keys,
70
+ "proxy_url": proxy_url,
71
+ "base_url": base_url
72
+ }
73
+
74
+ # 验证 service_api_keys 的格式
75
+ if not isinstance(config["service_api_keys"], list) or not all(isinstance(i, str) for i in config["service_api_keys"]):
76
+ raise TypeError("Secret 'SERVICE_API_KEYS' must be a JSON list of strings (e.g., [\"sk-key1\", \"sk-key2\"]).")
77
+
78
+ print("Configuration loaded successfully from secrets.")
79
+ print(f"Loaded {len(config['service_api_keys'])} service API key(s).")
80
+ print(f"Loaded {len(config['kodu_models'])} Kodu model mapping(s).")
81
+ if config["proxy_url"]:
82
+ print(f"Using proxy: {config['proxy_url']}")
83
+
84
+ except Exception as e:
85
+ print(f"FATAL: Could not load configuration from secrets. Error: {e}")
86
+ # 在配置失败时,将 config 设置为空字典以阻止应用不完整地运行
87
+ config = {}
88
+ raise
89
+
90
+ async def refresh_access_token():
91
+ """
92
+ 使用 refresh_token 获取新的 access_token.
93
+ """
94
+ if not config:
95
+ print("Cannot refresh token, config not loaded.")
96
+ return False
97
+
98
+ print("Attempting to refresh Kodu access token...")
99
+ async with httpx.AsyncClient(proxies=config.get("proxy_url")) as client:
100
+ try:
101
+ response = await client.post(
102
+ f"{config['base_url']}/auth/refresh-token",
103
+ headers={"Authorization": f"Bearer {config['kodu_refresh_token']}"}
104
+ )
105
+ response.raise_for_status()
106
+ config['kodu_access_token'] = response.json().get('accessToken')
107
+ print("Successfully refreshed Kodu access token.")
108
+ return True
109
+ except httpx.HTTPStatusError as e:
110
+ print(f"Error refreshing access token: {e.response.status_code} - {e.response.text}")
111
+ return False
112
+ except Exception as e:
113
+ print(f"An unexpected error occurred during token refresh: {e}")
114
+ return False
115
+
116
+ async def authenticate_client(credentials: HTTPAuthorizationCredentials = Depends(security)):
117
+ """
118
+ 验证客户端提供的 API Key.
119
+ """
120
+ if not config:
121
+ raise HTTPException(status_code=503, detail="Service Unavailable: Server configuration is not loaded.")
122
+
123
+ if credentials.credentials not in config.get("service_api_keys", []):
124
+ raise HTTPException(status_code=401, detail="Invalid API Key")
125
+
126
+ # --- FastAPI 事件和路由 ---
127
+
128
+ @app.on_event("startup")
129
+ async def startup_event():
130
+ """
131
+ 应用启动时执行的事件.
132
+ """
133
+ load_config()
134
+ if config:
135
+ await refresh_access_token()
136
+
137
+ @app.get("/v1/models", dependencies=[Depends(authenticate_client)])
138
+ async def list_models():
139
+ """
140
+ 列出可用的模型.
141
+ """
142
+ model_data = [
143
+ {"id": model_name, "object": "model", "owned_by": "kodu-ai", "created": int(time.time())}
144
+ for model_name in config.get("kodu_models", {})
145
+ ]
146
+ return {"object": "list", "data": model_data}
147
+
148
+ @app.post("/v1/chat/completions", dependencies=[Depends(authenticate_client)])
149
+ async def create_chat_completion(request: ChatCompletionRequest):
150
+ """
151
+ 处理聊天请求.
152
+ """
153
+ if not config.get('kodu_access_token'):
154
+ # 尝试再次刷新 token
155
+ if not await refresh_access_token():
156
+ raise HTTPException(status_code=503, detail="Kodu AI service unavailable, could not refresh token.")
157
+
158
+ kodu_model = config.get("kodu_models", {}).get(request.model)
159
+ if not kodu_model:
160
+ raise HTTPException(status_code=404, detail=f"Model '{request.model}' not found or mapped.")
161
+
162
+ payload = {
163
+ "model": kodu_model,
164
+ "messages": [{"role": msg.role, "content": msg.content} for msg in request.messages],
165
+ "stream": request.stream,
166
+ }
167
+
168
+ headers = {
169
+ "Authorization": f"Bearer {config['kodu_access_token']}",
170
+ "Content-Type": "application/json",
171
+ }
172
+
173
+ async def stream_generator():
174
+ async with httpx.AsyncClient(proxies=config.get("proxy_url"), timeout=300) as client:
175
+ try:
176
+ async with client.stream("POST", f"{config['base_url']}/chat/completions", headers=headers, json=payload) as response:
177
+ if response.status_code != 200:
178
+ error_content = await response.aread()
179
+ print(f"Kodu API Error: {response.status_code} - {error_content.decode()}")
180
+ # 尝试刷新 token 并重试一次
181
+ if response.status_code == 401:
182
+ print("Received 401, attempting to refresh token and retry...")
183
+ await refresh_access_token()
184
+ # 需要重新构建流请求,这里为了简化,直接返回错误
185
+ yield f"data: {json.dumps({'error': {'message': f'Kodu API Error: {error_content.decode()}'}})}\n\n"
186
+ yield "data: [DONE]\n\n"
187
+ return
188
+
189
+ async for chunk in response.aiter_bytes():
190
+ yield chunk.decode('utf-8')
191
+ except Exception as e:
192
+ print(f"An error occurred during streaming: {e}")
193
+ yield f"data: {json.dumps({'error': {'message': 'An internal error occurred.'}})}\n\n"
194
+ yield "data: [DONE]\n\n"
195
+
196
+ if request.stream:
197
+ return StreamingResponse(stream_generator(), media_type="text/event-stream")
198
+ else:
199
+ async with httpx.AsyncClient(proxies=config.get("proxy_url"), timeout=300) as client:
200
+ try:
201
+ response = await client.post(f"{config['base_url']}/chat/completions", headers=headers, json=payload)
202
+ if response.status_code == 401:
203
+ print("Received 401, refreshing token and retrying...")
204
+ await refresh_access_token()
205
+ headers["Authorization"] = f"Bearer {config['kodu_access_token']}"
206
+ response = await client.post(f"{config['base_url']}/chat/completions", headers=headers, json=payload)
207
+
208
+ response.raise_for_status()
209
+ return JSONResponse(content=response.json())
210
+ except httpx.HTTPStatusError as e:
211
+ raise HTTPException(status_code=e.response.status_code, detail=e.response.text)