codebee 0.1.20 → 0.1.21
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.
- package/CHANGELOG.md +6 -0
- package/README.md +8 -6
- package/app/core/automation.py +6 -6
- package/app/core/capability.py +16 -2
- package/app/core/catalog.py +1 -1
- package/app/core/covergen.py +40 -4
- package/app/core/dispatch.py +141 -0
- package/app/core/flows.py +5 -0
- package/app/core/jobs.py +281 -166
- package/app/core/manager.py +30 -23
- package/app/core/market_remote.py +66 -22
- package/app/core/modelhub.py +145 -11
- package/app/core/pipeline.py +111 -30
- package/app/core/router.py +18 -7
- package/app/core/runner.py +2 -1
- package/app/core/selfupdate.py +24 -14
- package/app/core/settings.py +3 -3
- package/app/core/skills.py +43 -0
- package/app/core/store.py +20 -14
- package/app/core/zentao.py +26 -6
- package/app/main.py +20 -12
- package/app/pet.py +2 -2
- package/app/ui/app.js +62 -58
- package/app/ui/i18n.js +21 -3
- package/app/ui/index.html +11 -11
- package/app/ui/style.css +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ README 元数据带回,供老版本在「发现新版本」时展示新版更
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## v0.1.21(2026-09-21)
|
|
10
|
+
|
|
11
|
+
- 任务默认直接启动,不再进入容量等待队列;达到并发保护上限立即失败并明确提示稍后重试,历史 queued 记录启动时自动接管或收口。
|
|
12
|
+
- 修复取消竞态:编排、管理、自升级、禅道修复在取消后不会复活;自动续跑重启恢复不再受隐藏数量上限影响,并显示明确的续跑时间。
|
|
13
|
+
- 定时任务、重试、续写、答疑和禅道修复统一使用同一套启动失败收口逻辑,避免任务长期停留在待启动状态。
|
|
14
|
+
|
|
9
15
|
## v0.1.20(2026-09-20)
|
|
10
16
|
|
|
11
17
|
- 全类型交付契约:小说、文章、短视频、文档、翻译、调研、演讲、汇报、邮件、技术方案、简历按真实类型路由,并注入各自角色、成品结构和真实性约束
|
package/README.md
CHANGED
|
@@ -21,8 +21,10 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
|
|
|
21
21
|
不会把你的任务内容交给任何第三方。
|
|
22
22
|
|
|
23
23
|
<!-- relnotes:start -->
|
|
24
|
-
### 最新版更新内容(v0.1.
|
|
24
|
+
### 最新版更新内容(v0.1.21)
|
|
25
25
|
|
|
26
|
+
- 任务默认直接启动,不进入容量等待队列;满载立即失败并提示稍后重试,自动续跑退避会显示预定时间
|
|
27
|
+
- 取消竞态修复:编排、管理、自升级、禅道修复取消后不会复活,重启可恢复全部定时续跑
|
|
26
28
|
- 11 类内容任务按真实类型路由,并带各自角色、成品结构和真实性约束
|
|
27
29
|
- token 预算恢复有效:默认路径与续会话均累计真实 usage,压缩重试不重复记账
|
|
28
30
|
- 15 种流程成本预估全覆盖;修复 rubric 串值、预置流程误建副本和 direct 标签
|
|
@@ -108,7 +110,7 @@ OpenCode / Continue / Cursor / Trae 十种工具的配置,一次带入全部
|
|
|
108
110
|
跑完整代码链,修完自动合并;双端问题修完我方部分后带修复报告转派另一端负责人,
|
|
109
111
|
非我方转回报告人;测试指错人也按排查结论改派;修复失败自动升级转派;群机器人
|
|
110
112
|
全程播报(设置页「禅道」子页配置);
|
|
111
|
-
-
|
|
113
|
+
- **多任务并发**:任务默认立即启动、无等待队列,并发保护上限默认 12;连载任务超时/中断自动
|
|
112
114
|
断点续跑;
|
|
113
115
|
- **远程访问**:手机同一 WiFi 直开、Tailscale 外网可达、Cloudflare Tunnel 固定
|
|
114
116
|
域名——8 位访问令牌保护,多端实时同步(SSE),单设备控制权防互踩;
|
|
@@ -316,10 +318,10 @@ start-public.bat
|
|
|
316
318
|
|
|
317
319
|
## 多任务并发
|
|
318
320
|
|
|
319
|
-
|
|
320
|
-
互不打扰——每个任务独立线程,运行数据按 run
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
任务执行器采用直接启动模式(并发保护上限默认 12,设置页「编排设置」可调 1-12):多个任务同时跑、
|
|
322
|
+
互不打扰——每个任务独立线程,运行数据按 run 隔离,不进入等待队列。达到保护上限时本次请求明确失败并提示稍后重试;并发数调小只影响后续任务,已有任务继续完成。
|
|
323
|
+
连载任务超时/中断会自动断点续跑;重试前按明确时间退避,防止在网关限流墙上连续撞死,
|
|
324
|
+
这段等待不占执行位,也不属于任务容量排队。
|
|
323
325
|
|
|
324
326
|
## 编排设置(编排者模型)
|
|
325
327
|
|
package/app/core/automation.py
CHANGED
|
@@ -292,7 +292,7 @@ def _recover_after_restart():
|
|
|
292
292
|
|
|
293
293
|
def _launch_run(t):
|
|
294
294
|
"""拉起一次真实编排运行:与 main.py 的 /api/tasks 走同一条链路
|
|
295
|
-
(store.create_task → store.create_run → jobs
|
|
295
|
+
(store.create_task → store.create_run → jobs 直接启动),返回 run_id。
|
|
296
296
|
测试可把本函数打成 stub,绝不真调 LLM/CLI。"""
|
|
297
297
|
payload = {"type": t.get("flow") or DEFAULT_FLOW,
|
|
298
298
|
"title": ("%s %s" % (t.get("name") or "定时任务",
|
|
@@ -309,15 +309,15 @@ def _launch_run(t):
|
|
|
309
309
|
"task_id": task["id"]})
|
|
310
310
|
return run["id"]
|
|
311
311
|
except Exception:
|
|
312
|
-
#
|
|
312
|
+
# 启动失败必须收口,不能留下看似仍在启动的任务/run。
|
|
313
313
|
# Keep the public error generic; the detailed traceback stays in the
|
|
314
314
|
# service log and the run record remains useful for diagnostics.
|
|
315
|
-
log.exception("automation:
|
|
315
|
+
log.exception("automation: 运行启动失败 task=%s run=%s",
|
|
316
316
|
(task or {}).get("id"), (run or {}).get("id"))
|
|
317
317
|
if run:
|
|
318
318
|
try:
|
|
319
319
|
store.update_run(run["id"], status="failed",
|
|
320
|
-
error="
|
|
320
|
+
error="任务启动失败,本次未排队,请稍后重试",
|
|
321
321
|
ended_at=time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
322
322
|
except Exception:
|
|
323
323
|
log.exception("automation: 运行失败收口失败 run=%s", run.get("id"))
|
|
@@ -334,7 +334,7 @@ def _launch_run(t):
|
|
|
334
334
|
def _fire(snapshot, now):
|
|
335
335
|
"""触发一个到期任务:拉起运行并推进 last_run/next_run/run_count/last_status。
|
|
336
336
|
once 触发完自动停用。拉起失败只记 error,next_run 照常推进(下个周期重试)。"""
|
|
337
|
-
status = "
|
|
337
|
+
status = "started"
|
|
338
338
|
try:
|
|
339
339
|
_launch_run(snapshot)
|
|
340
340
|
except Exception as e:
|
|
@@ -527,7 +527,7 @@ def run_now(tid):
|
|
|
527
527
|
return None, None
|
|
528
528
|
now = datetime.now()
|
|
529
529
|
run_id = ""
|
|
530
|
-
status = "
|
|
530
|
+
status = "started"
|
|
531
531
|
try:
|
|
532
532
|
with _LOCK:
|
|
533
533
|
snapshot = dict(_TASKS[tid])
|
package/app/core/capability.py
CHANGED
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import re
|
|
15
15
|
|
|
16
|
+
from .dispatch import TYPE_DIMENSIONS
|
|
17
|
+
|
|
16
18
|
# 能力维度(先固化清单,schema 见设计稿)
|
|
17
19
|
DIMS = ("writing", "coding", "reasoning", "vision")
|
|
18
20
|
|
|
@@ -33,6 +35,9 @@ def classify_task_type(task) -> str:
|
|
|
33
35
|
explicit = str((task or {}).get("task_type") or "").strip().lower()
|
|
34
36
|
if explicit in DIMS:
|
|
35
37
|
return explicit
|
|
38
|
+
preset = str((task or {}).get("type") or "").strip().lower()
|
|
39
|
+
if preset in TYPE_DIMENSIONS:
|
|
40
|
+
return TYPE_DIMENSIONS[preset]
|
|
36
41
|
text = "%s %s" % ((task or {}).get("type") or "",
|
|
37
42
|
(task or {}).get("goal") or "")
|
|
38
43
|
scores = {}
|
|
@@ -92,7 +97,8 @@ def resolve_binding_by_task(task, providers, bindings):
|
|
|
92
97
|
TIER_ORDER = {"budget": 0, "standard": 1, "premium": 2}
|
|
93
98
|
|
|
94
99
|
|
|
95
|
-
def cascade_reorder(agent, tier_of
|
|
100
|
+
def cascade_reorder(agent, tier_of, *, providers=None, pricing=None,
|
|
101
|
+
difficulty="easy", task_type="", role=""):
|
|
96
102
|
"""FrugalGPT 式便宜优先:easy 任务把 call_chain 按 tier 升序稳定重排。
|
|
97
103
|
|
|
98
104
|
agent 来自 modelhub.bind_agent(可能带 call_chain);tier_of(chain_entry)
|
|
@@ -102,6 +108,14 @@ def cascade_reorder(agent, tier_of):
|
|
|
102
108
|
chain = (agent or {}).get("call_chain") or []
|
|
103
109
|
if len(chain) < 2:
|
|
104
110
|
return agent
|
|
111
|
+
if providers is not None and (task_type or role):
|
|
112
|
+
from .dispatch import rank_model_entries
|
|
113
|
+
ranked, _decisions = rank_model_entries(
|
|
114
|
+
chain, {p.get("id"): p for p in providers or []},
|
|
115
|
+
pricing or {}, difficulty, task_type, role)
|
|
116
|
+
out = dict(agent)
|
|
117
|
+
out["call_chain"] = ranked
|
|
118
|
+
return out
|
|
105
119
|
|
|
106
120
|
def key(entry):
|
|
107
121
|
return TIER_ORDER.get(tier_of(entry) or "standard", 1)
|
|
@@ -127,4 +141,4 @@ def make_tier_lookup(providers):
|
|
|
127
141
|
if isinstance(m, dict) and m.get("name") == (entry or {}).get("model"):
|
|
128
142
|
return m.get("tier") or p.get("tier")
|
|
129
143
|
return p.get("tier")
|
|
130
|
-
return tier_of
|
|
144
|
+
return tier_of
|
package/app/core/catalog.py
CHANGED
|
@@ -154,7 +154,7 @@ DEFAULT_CATALOG = [
|
|
|
154
154
|
"note": "DeepSeek 官方 agent harness(dsh,profile 插件架构);无头是「一次性任务」——"
|
|
155
155
|
"答完即退、无交互后续、不支持会话恢复;任务只走位置参数(超长提示词受 Windows "
|
|
156
156
|
"命令行上限约 32k 约束);模型写进 ~/.dsh/settings.yaml 的 agent-default-model.model;"
|
|
157
|
-
"
|
|
157
|
+
"密钥由运行时自动调度或「模型调度(可选)」注入 DEEPSEEK_API_KEY(优先级最高);端点注入 DEEPSEEK_BASE_URL,"
|
|
158
158
|
"但若 settings.yaml 已固定 llm-deepseek.baseURL,则以它为准(settings 高于 env)",
|
|
159
159
|
"detect": {"cli": "dsh"},
|
|
160
160
|
"orch": {"kind": "generic", "command": "dsh",
|
package/app/core/covergen.py
CHANGED
|
@@ -42,17 +42,53 @@ _IMAGE_MODEL_KEYWORDS = ("cogview", "dall-e", "gpt-image", "flux", "kolors",
|
|
|
42
42
|
_SIZES = ("768x1344", "1024x1024") # 竖版优先,方图兜底
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _chapter_facts(workdir, limit=3):
|
|
46
|
+
"""从最新章节抽取视觉事实(借鉴 poster-v2 的 novel fact extraction):
|
|
47
|
+
人物/场景/关键意象的原文片段——封面元素来自真实剧情而非干瘪题材词。
|
|
48
|
+
读不到章节(单稿/未开写)返回空串,提示词回落题材+简介。"""
|
|
49
|
+
if not workdir:
|
|
50
|
+
return ""
|
|
51
|
+
import glob as _glob
|
|
52
|
+
try:
|
|
53
|
+
chapters = sorted(_glob.glob(str(workdir) + "/chapter-*.md"))
|
|
54
|
+
if not chapters:
|
|
55
|
+
return ""
|
|
56
|
+
facts = []
|
|
57
|
+
for p in chapters[-limit:]:
|
|
58
|
+
try:
|
|
59
|
+
with open(p, encoding="utf-8", errors="replace") as f:
|
|
60
|
+
txt = f.read(4000)
|
|
61
|
+
# 取开头(多为场景/人物动作)+ 关键句粗筛
|
|
62
|
+
head = " ".join(txt.split("\n")[1:6])
|
|
63
|
+
if head.strip():
|
|
64
|
+
facts.append(head.strip()[:200])
|
|
65
|
+
except OSError:
|
|
66
|
+
continue
|
|
67
|
+
return "\n".join(facts)[:900]
|
|
68
|
+
except Exception:
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
|
|
45
72
|
def _cover_prompt(task):
|
|
46
|
-
"""从任务与建书资料拼图像提示词:场景氛围向,不要文字(平台会自行压字)。
|
|
73
|
+
"""从任务与建书资料拼图像提示词:场景氛围向,不要文字(平台会自行压字)。
|
|
74
|
+
|
|
75
|
+
素材优先级:章节事实抽取(真实剧情的人物/场景/意象)> 题材+简介。
|
|
76
|
+
poster-v2 借鉴:视觉元素来自 novel facts 才有「这是这本书的封面」的辨识度。"""
|
|
47
77
|
bm = ((task.get("book_meta") or {}).get("fanqie") or {}).get("data") or {}
|
|
48
78
|
if not isinstance(bm, dict):
|
|
49
79
|
bm = {}
|
|
50
80
|
title = bm.get("书名") or task.get("title") or ""
|
|
51
81
|
genre = bm.get("类型") or bm.get("分类") or ""
|
|
52
82
|
brief = (bm.get("一句话简介") or bm.get("简介") or task.get("goal") or "")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
facts = _chapter_facts(task.get("workdir") or "")
|
|
84
|
+
base = ("竖版小说封面插画,画面中不要出现任何文字。题材:%s %s。故事梗概:%s。"
|
|
85
|
+
% (genre, title, str(brief)[:300]))
|
|
86
|
+
if facts:
|
|
87
|
+
base += ("\n\n以下是最新的剧情片段(从中提取主要人物形象、标志性场景与核心意象"
|
|
88
|
+
"作为画面主体——让封面一眼可辨「这是这本书」):\n%s" % facts)
|
|
89
|
+
base += ("\n商业网文封面质感:主体人物或核心场景突出,色彩浓郁有冲击力,"
|
|
90
|
+
"构图上方留白便于后期压标题。")
|
|
91
|
+
return base
|
|
56
92
|
|
|
57
93
|
|
|
58
94
|
def _pick_key(prov):
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""统一调度评分:任务画像、CLI 亲和度与模型链排序。
|
|
3
|
+
|
|
4
|
+
本模块只做纯计算,不读写配置和运行状态。调用方先完成协议、启停、健康、
|
|
5
|
+
密钥等硬约束过滤,再把可用候选交给这里评分;相同分数保持用户原顺序。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
TYPE_DIMENSIONS = {
|
|
13
|
+
"direct": "reasoning",
|
|
14
|
+
"code": "coding",
|
|
15
|
+
"novel": "writing",
|
|
16
|
+
"serial_novel": "writing",
|
|
17
|
+
"article": "writing",
|
|
18
|
+
"video_script": "writing",
|
|
19
|
+
"doc": "writing",
|
|
20
|
+
"translation": "writing",
|
|
21
|
+
"rank_scan": "reasoning",
|
|
22
|
+
"research": "reasoning",
|
|
23
|
+
"speech": "writing",
|
|
24
|
+
"weekly_report": "writing",
|
|
25
|
+
"email": "writing",
|
|
26
|
+
"tech_proposal": "reasoning",
|
|
27
|
+
"resume": "writing",
|
|
28
|
+
"zentao": "coding",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_KIND_AFFINITY = {
|
|
32
|
+
"coding": {"codex": 10, "aider": 8, "opencode": 7, "qwen": 6,
|
|
33
|
+
"claude": 5, "generic": 2},
|
|
34
|
+
"writing": {"claude": 10, "qwen": 7, "generic": 5, "codex": 3,
|
|
35
|
+
"opencode": 2, "aider": 0},
|
|
36
|
+
"reasoning": {"claude": 9, "codex": 8, "qwen": 6, "opencode": 4,
|
|
37
|
+
"generic": 3, "aider": 1},
|
|
38
|
+
"vision": {"codex": 10, "claude": 8, "qwen": 5, "opencode": 2,
|
|
39
|
+
"generic": 1, "aider": 0},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_TIER_SCORE = {
|
|
43
|
+
"easy": {"budget": 18.0, "standard": 8.0, "premium": -6.0},
|
|
44
|
+
"default": {"budget": 2.0, "standard": 6.0, "premium": 5.0},
|
|
45
|
+
"hard": {"budget": -8.0, "standard": 4.0, "premium": 12.0},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def task_dimension(task_type, role=""):
|
|
50
|
+
"""把预置任务与步骤角色归一为 writing/coding/reasoning/vision。"""
|
|
51
|
+
ttype = str(task_type or "").strip().lower()
|
|
52
|
+
role = str(role or "").strip().lower()
|
|
53
|
+
if ttype in ("writing", "coding", "reasoning", "vision"):
|
|
54
|
+
return ttype
|
|
55
|
+
if role in ("plan", "review", "critique", "selector", "qa") \
|
|
56
|
+
or "review" in role or "critique" in role:
|
|
57
|
+
return "reasoning"
|
|
58
|
+
return TYPE_DIMENSIONS.get(ttype, "reasoning")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def agent_affinity(kind, task_type, role=""):
|
|
62
|
+
"""CLI 类型对任务维度的温和偏好;只作加分,不覆盖健康与历史信号。"""
|
|
63
|
+
dim = task_dimension(task_type, role)
|
|
64
|
+
score = float((_KIND_AFFINITY.get(dim) or {}).get(kind, 0))
|
|
65
|
+
if role == "review" or "critique" in str(role or ""):
|
|
66
|
+
score += {"claude": 4, "codex": 3, "qwen": 2}.get(kind, 0)
|
|
67
|
+
return score, "%s 匹配 %+.1f" % (dim, score)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _model_meta(provider, model):
|
|
71
|
+
for item in (provider or {}).get("models") or []:
|
|
72
|
+
if isinstance(item, dict) and item.get("name") == model:
|
|
73
|
+
return item
|
|
74
|
+
return {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _price_score(pricing, model, difficulty):
|
|
78
|
+
price = (pricing or {}).get(model) or {}
|
|
79
|
+
try:
|
|
80
|
+
# 输出通常比输入贵且更影响整步成本,按 2 倍权重估算。
|
|
81
|
+
blended = max(0.0, float(price.get("in") or 0.0)
|
|
82
|
+
+ 2.0 * float(price.get("out") or 0.0))
|
|
83
|
+
except (TypeError, ValueError):
|
|
84
|
+
return 0.0, "价格未知"
|
|
85
|
+
if blended <= 0:
|
|
86
|
+
return 0.0, "价格未知"
|
|
87
|
+
magnitude = max(0.0, math.log10(blended + 1.0))
|
|
88
|
+
score = -min(18.0, magnitude * (7.0 if difficulty == "easy" else 2.0))
|
|
89
|
+
return score, "估算价 %.3g(%+.1f)" % (blended, score)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def score_model_entry(entry, providers, pricing, difficulty, task_type="", role=""):
|
|
93
|
+
"""给已通过硬约束的模型链条目评分,返回 (score, explanation)。"""
|
|
94
|
+
provider = (providers or {}).get(entry.get("provider_id")) \
|
|
95
|
+
or entry.get("provider") or {}
|
|
96
|
+
model = entry.get("model") or ""
|
|
97
|
+
meta = _model_meta(provider, model)
|
|
98
|
+
try:
|
|
99
|
+
priority = max(1, int(meta.get("priority") or 99))
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
priority = 99
|
|
102
|
+
quality = 0.0 if priority == 99 else max(-8.0, 20.0 - (priority - 1) * 4.0)
|
|
103
|
+
tier = meta.get("tier") or provider.get("tier") or "standard"
|
|
104
|
+
tier_score = (_TIER_SCORE.get(difficulty) or {}).get(tier, 0.0)
|
|
105
|
+
price_score, price_reason = _price_score(pricing, model, difficulty)
|
|
106
|
+
dim = task_dimension(task_type, role)
|
|
107
|
+
strengths = provider.get("strengths") if isinstance(provider.get("strengths"), list) else []
|
|
108
|
+
strength_score = 10.0 if dim in strengths else 0.0
|
|
109
|
+
if difficulty == "easy":
|
|
110
|
+
quality *= 0.25
|
|
111
|
+
elif difficulty != "hard":
|
|
112
|
+
# 普通任务兼顾质量、成本与能力匹配;是否真的重排由调用方决定,
|
|
113
|
+
# 因而手工链仍可保持原顺序,自动推荐则能使用这组平衡分。
|
|
114
|
+
quality *= 0.65
|
|
115
|
+
vision_score = 0.0
|
|
116
|
+
if dim == "vision":
|
|
117
|
+
vision_score = 24.0 if meta.get("image_in") else -24.0
|
|
118
|
+
total = quality + tier_score + price_score + strength_score + vision_score
|
|
119
|
+
reason = ("质量 %+.1f,档位 %s %+.1f,%s,能力 %s %+.1f"
|
|
120
|
+
% (quality, tier, tier_score, price_reason, dim,
|
|
121
|
+
strength_score + vision_score))
|
|
122
|
+
return round(total, 2), reason
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def rank_model_entries(entries, providers, pricing, difficulty,
|
|
126
|
+
task_type="", role="", force=False):
|
|
127
|
+
"""稳定排序模型链并返回脱敏决策明细;default 难度保持人工顺序。"""
|
|
128
|
+
rows = []
|
|
129
|
+
for index, entry in enumerate(entries or []):
|
|
130
|
+
score, reason = score_model_entry(
|
|
131
|
+
entry, providers, pricing, difficulty, task_type, role)
|
|
132
|
+
rows.append((score, index, entry, reason))
|
|
133
|
+
if difficulty in ("easy", "hard") or force:
|
|
134
|
+
rows.sort(key=lambda row: (-row[0], row[1]))
|
|
135
|
+
ranked = [row[2] for row in rows]
|
|
136
|
+
decisions = [{"provider_id": row[2].get("provider_id") or "",
|
|
137
|
+
"provider": (row[2].get("provider") or {}).get("name") or "",
|
|
138
|
+
"model": row[2].get("model") or "",
|
|
139
|
+
"score": row[0], "reason": row[3]}
|
|
140
|
+
for row in rows]
|
|
141
|
+
return ranked, decisions
|
package/app/core/flows.py
CHANGED
|
@@ -187,6 +187,11 @@ def _apply_overrides(base, ov):
|
|
|
187
187
|
f["rounds"] = max(1, min(5, int(v)))
|
|
188
188
|
except Exception:
|
|
189
189
|
pass
|
|
190
|
+
elif k == "best_of":
|
|
191
|
+
try:
|
|
192
|
+
f["best_of"] = max(1, min(3, int(v)))
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
190
195
|
elif k in ("manuscript", "verify_command"):
|
|
191
196
|
s = re.sub(r"[\\/]+", "_", str(v or "")).strip()
|
|
192
197
|
s = re.sub(r"\.{2,}", "_", s).lstrip(".")
|