lydgs commited on
Commit
f9bb630
·
verified ·
1 Parent(s): 89b7097

Update sync_cliproxy_cleanup.py

Browse files
Files changed (1) hide show
  1. sync_cliproxy_cleanup.py +258 -111
sync_cliproxy_cleanup.py CHANGED
@@ -1,159 +1,306 @@
 
1
  import os
2
  import time
3
  import requests
4
  from typing import List, Dict, Set
5
 
6
- # ---------- 环境变量 ----------
7
- LITELLM_BASE_URL = os.environ.get("LITELLM_BASE_URL", "http://localhost:7860")
8
- LITELLM_MASTER_KEY = os.environ["LITELLM_MASTER_KEY"]
9
- CLIPROXY_BASE_URL = os.environ["CLIPROXY_BASE_URL"]
10
- CLIPROXY_API_KEY = os.environ.get("CLIPROXY_API_KEY", "")
11
- CREDENTIAL_NAME = os.environ.get("LITELLM_CREDENTIAL_NAME", "cliproxy")
12
- FORCE_CREDENTIAL = os.environ.get("FORCE_USE_CREDENTIAL", "false").lower() == "true"
13
- PRIMARY_MODEL_GROUP = os.environ.get("FALLBACK_PRIMARY_MODEL", "cliproxy/*")
14
- SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL_SECONDS", 3600))
15
 
16
  SYNC_TAG = "cliproxy-synced"
17
- MASTER_HEADERS = {"Authorization": f"Bearer {LITELLM_MASTER_KEY}"}
18
 
19
- # ---------- provider 推断 ----------
20
- def infer_provider(owner: str, model_id: str) -> str:
21
- owner = owner.lower()
22
- model_id = model_id.lower()
 
 
 
 
23
 
24
- if "gemini" in model_id or owner == "google":
25
- return "gemini"
26
- if "claude" in model_id or owner == "anthropic":
27
- return "anthropic"
28
- if owner in ("moonshotai", "kimi"):
29
- return "openai" # Moonshot 兼容 OpenAI 协议
30
- if owner == "cliproxy":
31
- return "openai" # Cliproxy 本身是 OpenAI-compatible
32
- return "openai"
33
 
34
- # ---------- 等待 LiteLLM ----------
35
- def wait_for_litellm_ready(timeout: int = 180):
36
  print("⏳ 等待 LiteLLM Proxy 启动...")
37
  start = time.time()
38
  while time.time() - start < timeout:
39
  try:
40
- r = requests.get(f"{LITELLM_BASE_URL}/health", headers=MASTER_HEADERS, timeout=5)
41
- if r.status_code == 200:
42
  print("✅ LiteLLM Proxy 已就绪")
43
  return
44
- except Exception:
45
  pass
46
  time.sleep(5)
47
- raise RuntimeError("❌ LiteLLM Proxy 启动超时")
 
48
 
49
- # ---------- 凭证检查 ----------
50
- def credential_exists(name: str) -> bool:
51
- r = requests.get(f"{LITELLM_BASE_URL}/credentials", headers=MASTER_HEADERS)
52
- if not r.ok:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  return False
54
- data = r.json()
55
- creds = data["data"] if isinstance(data, dict) else data
56
- return any(c.get("credential_name") == name or c.get("name") == name for c in creds)
57
-
58
- # ---------- Cliproxy 模型 ----------
59
- def get_cliproxy_models() -> List[Dict]:
60
- headers = {}
61
- if CLIPROXY_API_KEY:
62
- headers["Authorization"] = f"Bearer {CLIPROXY_API_KEY}"
63
- r = requests.get(f"{CLIPROXY_BASE_URL}/models", headers=headers)
64
- r.raise_for_status()
65
- return r.json()["data"]
66
-
67
- # ---------- LiteLLM 模型 ----------
68
- def get_existing_models() -> List[Dict]:
69
- r = requests.get(f"{LITELLM_BASE_URL}/v1/models", headers=MASTER_HEADERS)
70
- r.raise_for_status()
71
- return r.json().get("data", [])
72
-
73
- # ---------- 删除模型 ----------
74
- def delete_model(model_name: str):
75
- r = requests.post(
76
- f"{LITELLM_BASE_URL}/model/delete",
77
- json={"model_name": model_name},
78
  headers=MASTER_HEADERS,
79
  )
80
- return r.status_code in (200, 204)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- # ---------- 添加模型(已修复) ----------
83
- def add_model_to_litellm(original_id: str, owner: str, use_credential: bool) -> bool:
84
- provider = infer_provider(owner, original_id)
85
- model_name = f"{owner}/{original_id}"
 
 
 
86
 
87
  litellm_params = {
88
  "model": f"{provider}/{original_id}",
89
  }
90
 
91
- # Gemini embedding 不支持 max_tokens
92
- if provider == "gemini" and "embed" in original_id.lower():
93
- pass
94
-
95
  if use_credential:
96
- litellm_params["litellm_credential_name"] = CREDENTIAL_NAME
 
97
  else:
98
- litellm_params["api_base"] = CLIPROXY_BASE_URL.rstrip("/") + "/v1"
99
- litellm_params["api_key"] = CLIPROXY_API_KEY
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  payload = {
102
- "model_name": model_name,
103
- "litellm_params": litellm_params,
104
- "model_info": {
105
- "owned_by": owner,
106
- "tags": [SYNC_TAG],
107
- },
108
  }
109
 
110
- r = requests.post(f"{LITELLM_BASE_URL}/model/new", json=payload, headers=MASTER_HEADERS)
111
- if r.status_code in (200, 201):
112
- print(f" ➕ 新增模型 {model_name}")
 
 
 
 
113
  return True
 
 
 
114
 
115
- print(f" ❌ 添加失败 {model_name}: {r.status_code} {r.text}")
116
- return False
117
 
118
- # ---------- 同步 ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  def sync():
120
- print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 开始同步")
121
 
122
- use_credential = FORCE_CREDENTIAL or credential_exists(CREDENTIAL_NAME)
 
 
123
 
124
- clip_models = get_cliproxy_models()
125
- clip_map = {f"{m['owned_by']}/{m['id']}": m["owned_by"] for m in clip_models}
 
 
 
 
 
126
 
127
- existing = get_existing_models()
128
- existing_ids = {m["id"] for m in existing}
 
129
 
130
- for full_name, owner in clip_map.items():
131
- if full_name not in existing_ids:
132
- add_model_to_litellm(full_name.split("/", 1)[1], owner, use_credential)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- # 删除失效模型
135
- for m in existing:
136
- if SYNC_TAG in m.get("tags", []) and m["id"] not in clip_map:
137
- delete_model(m["id"])
 
 
 
138
 
139
- # fallback embedding
140
- fallback_models = [m for m in clip_map if "embed" not in m.lower()]
141
- requests.post(
142
- f"{LITELLM_BASE_URL}/fallback",
143
- json={
144
- "model": PRIMARY_MODEL_GROUP,
145
- "fallback_models": fallback_models[:50],
146
- "fallback_type": "general",
147
- },
148
- headers=MASTER_HEADERS,
149
- )
150
 
151
- print("✅ 同步完成\n")
152
 
153
- # ---------- 守护 ----------
154
- if __name__ == "__main__":
155
- wait_for_litellm_ready()
156
- print(f"🚀 同步守护启动, {SYNC_INTERVAL}s 执行一次")
157
  while True:
158
  sync()
159
- time.sleep(SYNC_INTERVAL)
 
 
1
+ `python
2
  import os
3
  import time
4
  import requests
5
  from typing import List, Dict, Set
6
 
7
+ ---------- 环境变量 ----------
8
+ LITELLMBASEURL = os.environ.get("LITELLMBASEURL", "http://localhost:7860")
9
+ LITELLMMASTERKEY = os.environ["LITELLMMASTERKEY"]
10
+ CLIPROXYBASEURL = os.environ["CLIPROXYBASEURL"]
11
+ CLIPROXYAPIKEY = os.environ.get("CLIPROXYAPIKEY", "")
12
+ CREDENTIALNAME = os.environ.get("LITELLMCREDENTIAL_NAME", "cliproxy")
13
+ FORCECREDENTIAL = os.environ.get("FORCEUSE_CREDENTIAL", "false").lower() == "true"
14
+ PRIMARYMODELGROUP = os.environ.get("FALLBACKPRIMARYMODEL", "cliproxy/*")
15
+ SYNCINTERVAL = int(os.environ.get("SYNCINTERVAL_SECONDS", 3600))
16
 
17
  SYNC_TAG = "cliproxy-synced"
18
+ MASTERHEADERS = {"Authorization": f"Bearer {LITELLMMASTER_KEY}"}
19
 
20
+ provider 优先级(用于 fallback 排序)
21
+ PROVIDER_PRIORITY = {
22
+ "cliproxy": 0,
23
+ "moonshotai": 1,
24
+ "kimi": 1,
25
+ "anthropic": 2,
26
+ "google": 3,
27
+ }
28
 
 
 
 
 
 
 
 
 
 
29
 
30
+ ---------- 等待 LiteLLM 就绪 ----------
31
+ def waitforlitellm_ready(timeout: int = 180):
32
  print("⏳ 等待 LiteLLM Proxy 启动...")
33
  start = time.time()
34
  while time.time() - start < timeout:
35
  try:
36
+ resp = requests.get(f"{LITELLMBASEURL}/health", headers=MASTER_HEADERS, timeout=5)
37
+ if resp.status_code == 200:
38
  print("✅ LiteLLM Proxy 已就绪")
39
  return
40
+ except requests.RequestException:
41
  pass
42
  time.sleep(5)
43
+ raise RuntimeError(f"❌ LiteLLM Proxy 未在 {timeout}s 内就绪")
44
+
45
 
46
+ ---------- 检查凭证是否可用 ----------
47
+ def credentialexists(credentialname: str) -> bool:
48
+ try:
49
+ resp = requests.get(f"{LITELLMBASEURL}/credentials", headers=MASTER_HEADERS)
50
+ if resp.status_code != 200:
51
+ print(f"⚠️ 获取凭证列表失败,HTTP {resp.status_code}: {resp.text}")
52
+ return False
53
+
54
+ data = resp.json()
55
+ if isinstance(data, list):
56
+ credentials = data
57
+ elif isinstance(data, dict) and "data" in data:
58
+ credentials = data["data"]
59
+ else:
60
+ print(f"⚠️ 无法识别的凭证列表格式: {data}")
61
+ return False
62
+
63
+ for cred in credentials:
64
+ if cred.get("credentialname") == credentialname or cred.get("name") == credential_name:
65
+ print(f"✅ 凭证 '{credential_name}' 存在")
66
+ return True
67
+ print(f"⚠️ 凭证 '{credential_name}' 不在列表中")
68
+ return False
69
+ except Exception as e:
70
+ print(f"⚠️ 检查凭证时出错: {e}")
71
  return False
72
+
73
+
74
+ ---------- CLIProxy 模型获取 ----------
75
+ def getcliproxymodels() -> List = {}
76
+ if CLIPROXYAPIKEY:
77
+ headers["Authorization"] = f"Bearer {CLIPROXYAPIKEY}"
78
+ resp = requests.get(f"{CLIPROXYBASEURL}/models", headers=headers)
79
+ resp.raiseforstatus()
80
+ return resp.json()["data"]
81
+
82
+
83
+ ---------- LiteLLM 现有模型 ----------
84
+ def getexistingmodels_full() -> List[Dict]:
85
+ resp = requests.get(f"{LITELLMBASEURL}/v1/models", headers=MASTER_HEADERS)
86
+ resp.raiseforstatus()
87
+ return resp.json().get("data", [])
88
+
89
+
90
+ ---------- 模型删除 ----------
91
+ def deletemodel(modelname: str) -> bool:
92
+ resp = requests.post(
93
+ f"{LITELLMBASEURL}/model/delete",
94
+ json={"modelname": modelname},
 
95
  headers=MASTER_HEADERS,
96
  )
97
+ if resp.status_code in [200, 204]:
98
+ print(f" 🗑️ 成功删除模型: {model_name}")
99
+ return True
100
+ else:
101
+ print(f" ❌ 删除模型 {modelname} 失败,状态码: {resp.statuscode}, 响应: {resp.text}")
102
+ return False
103
+
104
+
105
+ ---------- 判断是否为同步模型(通过标签或 owner)----------
106
+ def issyncedmodel(modelid: str, modelsfull: List[Dict]) -> bool:
107
+ for model in models_full:
108
+ if model["id"] == model_id:
109
+ if "tags" in model and SYNC_TAG in model["tags"]:
110
+ return True
111
+ owner = model.get("owned_by", "unknown")
112
+ if "/" in model_id and owner != "openai":
113
+ return True
114
+ return False
115
+
116
+
117
+ ---------- 推断 provider ----------
118
+ def inferprovider(owner: str, modelid: str) -> str:
119
+ owner = owner.lower()
120
+ modelid = modelid.lower()
121
+
122
+ # cliproxy 永远走 OpenAI 兼容协议
123
+ if owner == "cliproxy":
124
+ return "openai"
125
+
126
+ if "gemini" in model_id or owner == "google":
127
+ return "gemini"
128
+ if "claude" in model_id or owner == "anthropic":
129
+ return "anthropic"
130
+ if owner in ("moonshotai", "kimi"):
131
+ return "openai"
132
+
133
+ return "openai"
134
+
135
+
136
+ ---------- 系统模型过滤(防止 openai/container 之类混入 fallback) ----------
137
+ def issystemmodel(model_name: str) -> bool:
138
+ name = model_name.lower()
139
+ return (
140
+ name.startswith("container")
141
+ or name.startswith("hf")
142
+ or name.startswith("litellm")
143
+ or name.startswith("internal")
144
+ )
145
+
146
+
147
+ ---------- fallback 排序 ----------
148
+ def sortfallbackmodels(models: Dict[str, str]) -> list[str]:
149
+ """
150
+ models: {model_name: owner}
151
+ """
152
+ def score(item):
153
+ model_name, owner = item
154
+ owner = owner.lower()
155
+
156
+ # embedding 永远排除(后面还会再过滤一遍)
157
+ if "embed" in model_name.lower():
158
+ return (99, model_name)
159
+
160
+ priority = PROVIDER_PRIORITY.get(owner, 50)
161
+ return (priority, model_name)
162
 
163
+ return [name for name, _ in sorted(models.items(), key=score)]
164
+
165
+
166
+ ---------- 模型添加 ----------
167
+ def addmodeltolitellm(originalid: str, owner: str, use_credential: bool) -> bool:
168
+ provider = inferprovider(owner, originalid)
169
+ newname = f"{owner}/{originalid}" if not originalid.startswith(f"{owner}/") else originalid
170
 
171
  litellm_params = {
172
  "model": f"{provider}/{original_id}",
173
  }
174
 
175
+ # Cliproxy / OpenAI 兼容:使用自定义 api_base + key
 
 
 
176
  if use_credential:
177
+ litellmparams["litellmcredentialname"] = CREDENTIALNAME
178
+ print(f" 🔑 新模型 {newname} 将引用凭证 '{CREDENTIALNAME}'")
179
  else:
180
+ litellmparams["apibase"] = CLIPROXYBASEURL.rstrip("/") + "/v1"
181
+ litellmparams["apikey"] = CLIPROXYAPIKEY
182
+ print(f" 🔑 新模型 {new_name} 将使用环境变量中的 API Key (凭证回退)")
183
+
184
+ model_info = {
185
+ "owned_by": owner,
186
+ "tags": [SYNC_TAG],
187
+ }
188
+
189
+ # 标记模型类型,避免 LiteLLM 用 completion 去 probe embedding
190
+ if "embed" in original_id.lower():
191
+ modelinfo["modeltype"] = "embedding"
192
+ else:
193
+ modelinfo["modeltype"] = "chat"
194
 
195
  payload = {
196
+ "modelname": newname,
197
+ "litellmparams": litellmparams,
198
+ "modelinfo": modelinfo,
 
 
 
199
  }
200
 
201
+ resp = requests.post(
202
+ f"{LITELLMBASEURL}/model/new",
203
+ json=payload,
204
+ headers=MASTER_HEADERS,
205
+ )
206
+ if resp.status_code in (200, 201):
207
+ print(f" ➕ 新增: {new_name}")
208
  return True
209
+ else:
210
+ print(f" ❌ 添加模型 {newname} 失败: {resp.statuscode} {resp.text}")
211
+ return False
212
 
 
 
213
 
214
+ ---------- 更新 Fallback 链 ----------
215
+ def updatefallbackchain(model_names: List[str]):
216
+ # 先清空旧规则
217
+ requests.post(
218
+ f"{LITELLMBASEURL}/fallback/delete",
219
+ json={"model": PRIMARYMODELGROUP, "fallback_type": "general"},
220
+ headers=MASTER_HEADERS,
221
+ )
222
+
223
+ if not model_names:
224
+ print("⚠️ 没有可用的 Fallback 模型,已清空规则。")
225
+ return
226
+
227
+ fallbackmodels = modelnames[:50]
228
+ payload = {
229
+ "model": PRIMARYMODELGROUP,
230
+ "fallbackmodels": fallbackmodels,
231
+ "fallback_type": "general",
232
+ }
233
+ resp = requests.post(f"{LITELLMBASEURL}/fallback", json=payload, headers=MASTER_HEADERS)
234
+ if resp.ok:
235
+ print(f"✅ Fallback 已更新,包含 {len(fallback_models)} 个模型。")
236
+ else:
237
+ print(f"❌ Fallback 更新失败: {resp.status_code} {resp.text}")
238
+
239
+
240
+ ---------- 主同步流程 ----------
241
  def sync():
242
+ print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 开始同步")
243
 
244
+ usecredential = FORCECREDENTIAL or credentialexists(CREDENTIALNAME)
245
+ if not use_credential:
246
+ print(f"⚠️ 凭证 '{CREDENTIAL_NAME}' 未找到,将使用环境变量中的 API Key")
247
 
248
+ # 1. 获取 Cliproxy 模型
249
+ cliproxymodels = getcliproxy_models()
250
+ cliproxy_map: Dict[str, str] = {}
251
+ for m in cliproxy_models:
252
+ owner = m.get("owned_by", "cliproxy")
253
+ new_name = f"{owner}/{m['id']}"
254
+ cliproxymap[newname] = owner
255
 
256
+ # 2. 获取 LiteLLM 现有模型
257
+ existingfull = getexistingmodelsfull()
258
+ existingids: Set[str] = {m["id"] for m in existingfull}
259
 
260
+ # 3. 新增 / 迁移模型
261
+ added = 0
262
+ migrated = 0
263
+ for newname, owner in cliproxymap.items():
264
+ if newname not in existingids:
265
+ if addmodeltolitellm(newname.split("/", 1)[1], owner, use_credential):
266
+ added += 1
267
+ else:
268
+ # 如果需要凭证,则通过“删除+重新添加”的方式迁移
269
+ if use_credential:
270
+ originalid = newname.split("/", 1)[1]
271
+ if deletemodel(newname):
272
+ if addmodeltolitellm(originalid, owner, use_credential=True):
273
+ print(f" 🔄 模型 {new_name} 已迁移为凭证模式")
274
+ migrated += 1
275
+ else:
276
+ print(f" ❌ 迁移 {new_name} 失败:重新添加失败")
277
+ else:
278
+ print(f" ⚠️ 无法删除模型 {new_name},跳过迁移")
279
 
280
+ # 4. 删除失效模型(通过标签识别)
281
+ deleted = 0
282
+ for modelfull in existingfull:
283
+ modelid = modelfull["id"]
284
+ if issyncedmodel(modelid, existingfull) and modelid not in cliproxymap:
285
+ if deletemodel(modelid):
286
+ deleted += 1
287
 
288
+ # 5. 生成 fallback 列表: + 过滤 embedding + 过滤系统模型
289
+ sortedmodels = sortfallbackmodels(cliproxymap)
290
+ fallback_candidates = [
291
+ m for m in sorted_models
292
+ if "embed" not in m.lower() and not issystemmodel(m)
293
+ ]
294
+ updatefallbackchain(fallback_candidates)
295
+
296
+ print(f"✅ 同步完成:新增 {added}, 迁移凭证 {migrated}, 删除 {deleted}\n")
 
 
297
 
 
298
 
299
+ ---------- 守护���程 ----------
300
+ if name == "main":
301
+ waitforlitellm_ready()
302
+ print("🚀 守护同步任务启动,间隔 " + str(SYNC_INTERVAL) + " 秒")
303
  while True:
304
  sync()
305
+ time.sleep(SYNC_INTERVAL)
306
+ `