codebee 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -270,7 +270,9 @@ def refresh_models(provider_id):
270
270
  "priority": o.get("priority", 0),
271
271
  "hidden": bool(o.get("hidden")),
272
272
  # 模态声明同理:用户/预填设过的 image_in 刷新时不能被抹掉
273
- "image_in": bool(o.get("image_in"))}
273
+ "image_in": bool(o.get("image_in")),
274
+ # 手工添加的标记也要透传:它决定刷新时「不在列表里」是否保留
275
+ "manual": bool(o.get("manual"))}
274
276
  (hidden if item["hidden"] else existing).append(item)
275
277
  else:
276
278
  fresh.append({"name": n, "enabled": True,
@@ -279,6 +281,10 @@ def refresh_models(provider_id):
279
281
  # 已删除但本次未返回的条目也保留,否则下次拉取会当作新模型“复活”
280
282
  hidden += [dict(o) for n, o in old.items()
281
283
  if o.get("hidden") and n not in names]
284
+ # 手工添加但厂商列表里没有的模型同样保留(有的网关列表接口调不通或只返回
285
+ # 部分),否则一次成功的刷新会把用户手工接入的模型静默清掉。
286
+ existing += [dict(o) for n, o in old.items()
287
+ if o.get("manual") and not o.get("hidden") and n not in names]
282
288
  # 顺序 = 既有启用(保持用户手动排序)+ 新模型(按强弱估分)+ 既有停用 + 已删除墓碑。
283
289
  # 启用块始终在停用块之前(顺带自愈历史数据);新模型不能插到启用块前面,
284
290
  # 否则会顶掉手动顺序与「#1 即默认模型」语义。
@@ -305,6 +311,43 @@ def refresh_models(provider_id):
305
311
  return len(allm), ""
306
312
 
307
313
 
314
+ def add_model_manual(provider_id, name):
315
+ """手工添加模型——厂商列表接口调不通(或只返回部分)时的通路。返回 (可见模型数, 错误)。
316
+
317
+ 不要求密钥、不发网络请求:模型能不能用交给「测试连接」和编排时的换将去验证。
318
+ 名字已存在且可见 → 不重复加;已存在但是删除墓碑 → 视同恢复。
319
+ 打 manual 标记:之后刷新即使厂商列表里没有它也保留,不会被清掉。
320
+ 新模型排在启用块末尾(不置顶、不抢 #1 默认模型),不改变既有排序语义。
321
+ """
322
+ name = (name or "").strip()
323
+ if not name:
324
+ return 0, "模型名不能为空"
325
+ if len(name) > 200 or any(c in name for c in "\r\n\t"):
326
+ return 0, "模型名不合法"
327
+ with _LOCK:
328
+ data = _load()
329
+ prov = _find_prov(data, provider_id)
330
+ if not prov:
331
+ return 0, "供应商不存在"
332
+ models = prov.get("models")
333
+ if models is None:
334
+ models = []
335
+ prov["models"] = models
336
+ target = next((m for m in models if m.get("name") == name), None)
337
+ if target and not target.get("hidden"):
338
+ return len(_ranked(models)), "模型已存在"
339
+ if target:
340
+ target["hidden"] = False
341
+ target["enabled"] = True
342
+ target["manual"] = True
343
+ else:
344
+ models.append({"name": name, "enabled": True,
345
+ "image_in": _auto_image_in(name), "manual": True})
346
+ _promote(models) # 启用块末尾:参与编排但不抢 #1 默认模型
347
+ _save(data)
348
+ return len(_ranked(models)), ""
349
+
350
+
308
351
  def refresh_all_async():
309
352
  """后台逐个刷新全部供应商的模型列表(导入后自动触发)。返回供应商数。
310
353
 
@@ -1985,6 +2028,23 @@ def _protocol_candidates(prov):
1985
2028
  if (caps.get(p) or {}).get("base")]
1986
2029
 
1987
2030
 
2031
+ def _ensure_wire_candidates(provider_id, prov, model_name=""):
2032
+ """唯一协议场景(单模型测试/直连对话)的鸡生蛋解除:auto 供应商还没探出
2033
+ wire_caps 时(导入后没探过/上次探挂了),带着手头模型先实测一轮再谈——
2034
+ 探到即落盘放行;返回 (候选, 失败原因 note),仍探不出则 ([], note)。"""
2035
+ protos = _protocol_candidates(prov)
2036
+ if protos:
2037
+ return protos, ""
2038
+ note = ""
2039
+ try:
2040
+ _, note = probe_wire_caps(provider_id, prefer_model=model_name)
2041
+ except Exception as e:
2042
+ note = repr(e)[:160]
2043
+ with _LOCK:
2044
+ prov2 = next((p for p in providers() if p.get("id") == provider_id), None)
2045
+ return (_protocol_candidates(prov2) if prov2 else []), (note or "")
2046
+
2047
+
1988
2048
  def bindable_protocols(agent_kind_or_id):
1989
2049
  """该 CLI 可绑定的 wire 协议(与 resolve_binding 的 allowed 一致)。
1990
2050
  供死链告警/失败文案解释「为什么绑不上」:claude 只认 anthropic,
@@ -2445,7 +2505,8 @@ def test_provider(provider_id):
2445
2505
  def test_model(provider_id, model_name, key_id=""):
2446
2506
  """单模型连通性测试:发一条 1 token 的最小对话。返回 {ok, latency_ms, error}。
2447
2507
 
2448
- auto 供应商逐条试实测过的 wire(显式协议只有一条);多 KEY 供应商逐把试
2508
+ auto 供应商逐条试实测过的 wire(显式协议只有一条;还没探过 wire_caps
2509
+ 先自带一次适配探测,见 _ensure_wire_candidates);多 KEY 供应商逐把试
2449
2510
  (指定 key_id 则只测那把)。返回里带 protocol/key_id 说明这次是谁通的。
2450
2511
  """
2451
2512
  import time as _t
@@ -2454,9 +2515,10 @@ def test_model(provider_id, model_name, key_id=""):
2454
2515
  prov = next((p for p in providers() if p.get("id") == provider_id), None)
2455
2516
  if not prov or not prov.get("api_key"):
2456
2517
  return {"ok": False, "error": "供应商不存在或未配置密钥"}
2457
- protos = _protocol_candidates(prov)
2518
+ protos, note = _ensure_wire_candidates(provider_id, prov, model_name)
2458
2519
  if not protos:
2459
- return {"ok": False, "error": "该供应商还没有可用 wire——先「获取模型列表」或手动指定格式"}
2520
+ return {"ok": False,
2521
+ "error": "没探到可用 wire——地址/密钥/模型名至少一项不通" + ("(%s)" % note if note else "")}
2460
2522
  if key_id:
2461
2523
  keys = [k for k in _provider_keys(prov) if k["id"] == key_id]
2462
2524
  if not keys:
@@ -2573,13 +2635,16 @@ def _probe_wire_once(base, api_key, target_proto, model, allow_private, timeout=
2573
2635
  return False, "", last
2574
2636
 
2575
2637
 
2576
- def probe_wire_caps(provider_id):
2638
+ def probe_wire_caps(provider_id, prefer_model=""):
2577
2639
  """适配测试:实测该供应商的可用 wire,存进 wire_caps。返回 (caps, note)。
2578
2640
 
2579
2641
  显式协议的供应商只测「除原生外」的 wire(原生天然可用,不必花请求);
2580
2642
  protocol="auto"(导入未指定格式)则把可注入 wire 全测一遍,caps 就是分类
2581
2643
  结果。之前通过、本次失败的条目移除(网关两面变动以实测为准)。google 不参与。
2582
- note 是给 UI 的补充说明(失败原因 / 未测原因),全通过时为空串。"""
2644
+ prefer_model 优先作探针(单模型测试/直连对话带着手头模型来探);否则用
2645
+ 手动填的模型 > 首个启用模型 > 首个可见模型——探针只是 1 token 连通验证,
2646
+ 全停用也能探,wire 能力是供应商级属性。note 是给 UI 的补充说明(失败原因 /
2647
+ 未测原因),全通过时为空串。"""
2583
2648
  import time as _t
2584
2649
  with _LOCK:
2585
2650
  prov = next((p for p in providers() if p.get("id") == provider_id), None)
@@ -2587,9 +2652,12 @@ def probe_wire_caps(provider_id):
2587
2652
  return {}, "供应商不存在"
2588
2653
  if not prov.get("api_key"):
2589
2654
  return {}, "该供应商未配置密钥"
2590
- model = prov.get("model") or next(
2591
- (m.get("name") for m in _ranked(prov.get("models") or [])
2592
- if m.get("name") and m.get("enabled", True)), "")
2655
+ ranked = _ranked(prov.get("models") or [])
2656
+ model = (prefer_model or prov.get("model") or next(
2657
+ (m.get("name") for m in ranked
2658
+ if m.get("name") and m.get("enabled", True)), ""))
2659
+ if not model:
2660
+ model = next((m.get("name") for m in ranked if m.get("name")), "")
2593
2661
  if not model:
2594
2662
  return {}, "没有可用模型名——先「获取模型列表」再测"
2595
2663
  native = prov.get("protocol")
@@ -2758,10 +2826,11 @@ def chat(provider_id, model_name, prompt, max_tokens=2048, timeout=120, cache_tt
2758
2826
  if not prov or not prov.get("api_key"):
2759
2827
  return {"ok": False, "text": "", "tokens": 0, "usage": None,
2760
2828
  "error": "供应商不存在或未配置密钥"}
2761
- protos = _protocol_candidates(prov)
2829
+ protos, note = _ensure_wire_candidates(provider_id, prov, model_name)
2762
2830
  if not protos:
2763
2831
  return {"ok": False, "text": "", "tokens": 0, "usage": None,
2764
- "error": "该供应商还没有可用 wire——先「获取模型列表」或手动指定格式"}
2832
+ "error": "没探到可用 wire——地址/密钥/模型名至少一项不通"
2833
+ + ("(%s)" % note if note else "")}
2765
2834
  # 多 KEY:按「KEY 序 × 协议」展开逐条试。欠费的 KEY 先被跳过(切备用),
2766
2835
  # 冷却中的 KEY 一条都不剩时仍按原顺序试——比整家供应商不可用强。
2767
2836
  keys = _chain_keys(prov) or [{"key": prov.get("api_key") or "", "id": ""}]
package/app/core/paths.py CHANGED
@@ -55,6 +55,7 @@ DATA_DIR = default_data_dir()
55
55
  TASKS_DIR = DATA_DIR / "tasks"
56
56
  RUNS_DIR = DATA_DIR / "runs"
57
57
  USAGE_DIR = DATA_DIR / "usage"
58
+ ERRORS_DIR = DATA_DIR / "errors"
58
59
  CATALOG_FILE = DATA_DIR / "catalog.json"
59
60
  ENABLED_FILE = DATA_DIR / "orchestration.json"
60
61