trantor 0.17.44 → 0.17.46
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/bin/advise.mjs +22 -5
- package/bin/crew-runner.mjs +6 -0
- package/bin/crew.sh +4 -1
- package/bin/doctor.mjs +4 -0
- package/engine/bin/scrooge +15 -4
- package/engine/bin/scrooge-capabilities +71 -5
- package/engine/test-routing.py +55 -0
- package/package.json +3 -3
- package/skills/crew/SKILL.md +6 -0
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + context-handoff for independent AI coding agents (Claude, Codex, Gemini, …)",
|
|
9
|
-
"version": "0.17.
|
|
9
|
+
"version": "0.17.46"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, GLM, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.46",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.46",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/bin/advise.mjs
CHANGED
|
@@ -35,6 +35,14 @@ export const ROSTER = {
|
|
|
35
35
|
kimi: { cli: "kimi", launch: "kimi", session: "kimi", provider: "kimi" },
|
|
36
36
|
deepseek: { cli: "opencode", launch: "deepseek:deepseek", session: "deepseek", provider: "deepseek" },
|
|
37
37
|
glm: { cli: "opencode", launch: "opencode:zai-coding-plan", session: "opencode", provider: "zai" },
|
|
38
|
+
// OpenRouter = the BYOM on-ramp: one key fronts hundreds of models (incl. vendors with no
|
|
39
|
+
// CLI of their own). Rides the opencode runner like glm/deepseek, but under its OWN bus label
|
|
40
|
+
// `openrouter` (distinct session, never collides with the glm `opencode` seat). The launcher
|
|
41
|
+
// live-selects the best OpenRouter model for the work's difficulty (or pin one:
|
|
42
|
+
// `openrouter:openrouter/<vendor>/<model>`). The model picked is what determines its strength,
|
|
43
|
+
// so it sits LAST in every CREW_PREF tier — a wildcard that fills once the proven native seats
|
|
44
|
+
// are taken, and the ONLY seat for a user who brought nothing but an OpenRouter key.
|
|
45
|
+
openrouter: { cli: "opencode", launch: "openrouter:openrouter", session: "openrouter", provider: "openrouter" },
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
export function loadWorld() {
|
|
@@ -42,12 +50,17 @@ export function loadWorld() {
|
|
|
42
50
|
const registry = read(join(H, ".token-scrooge", "registry.json"), { models: {}, tasks: {} });
|
|
43
51
|
const caps = read(join(H, ".token-scrooge", "capabilities.json"), {});
|
|
44
52
|
const has = (c) => { try { execSync(`command -v ${c}`, { stdio: "ignore", shell: "/bin/sh" }); return true; } catch { return false; } };
|
|
45
|
-
const opencodeKey = () => !!read(join(H, ".config", "opencode", "opencode.json"), {})?.provider?.[
|
|
53
|
+
const opencodeKey = (prov) => !!read(join(H, ".config", "opencode", "opencode.json"), {})?.provider?.[prov]?.options?.apiKey;
|
|
54
|
+
// a key the user already has for Scrooge counts too — the opencode runner sources these .env
|
|
55
|
+
// files, so OPENROUTER_API_KEY in ~/.token-scrooge/.env lights up the crew seat with no extra setup.
|
|
56
|
+
const envHasKey = (k) => !!process.env[k] || [join(H, ".token-scrooge", ".env"), join(H, ".agent-bus", ".env")]
|
|
57
|
+
.some(f => { try { return readFileSync(f, "utf8").includes(k); } catch { return false; } });
|
|
46
58
|
// a seat is available only if its CLI exists AND its provider is actually set up — a present
|
|
47
|
-
// binary with a dead/missing seat (gemini, or opencode with no
|
|
59
|
+
// binary with a dead/missing seat (gemini, or opencode with no provider key) must NOT be recommended.
|
|
48
60
|
const hasSeat = (tok) => {
|
|
49
61
|
const r = ROSTER[tok]; if (!r || !has(r.cli)) return false;
|
|
50
|
-
if (tok === "glm") return !!profile?.providers?.zai || opencodeKey();
|
|
62
|
+
if (tok === "glm") return !!profile?.providers?.zai || opencodeKey("zai-coding-plan");
|
|
63
|
+
if (tok === "openrouter") return !!profile?.providers?.openrouter || opencodeKey("openrouter") || envHasKey("OPENROUTER_API_KEY");
|
|
51
64
|
return true;
|
|
52
65
|
};
|
|
53
66
|
const agents = Object.keys(ROSTER).filter(hasSeat);
|
|
@@ -70,7 +83,7 @@ export function scroogeModelFor(registry, caps, kind = "code", difficulty = "eas
|
|
|
70
83
|
const FORECAST = { easy: 0.3e6, medium: 1.5e6, hard: 6e6 }; // tokens
|
|
71
84
|
// crew agent preference per difficulty: frontier subs take hard, cheap takes easy.
|
|
72
85
|
// (gemini retired → its slot goes to glm, a strong coding-plan seat on $0 marginal quota.)
|
|
73
|
-
const CREW_PREF = { hard: ["codex", "glm", "kimi", "deepseek"], medium: ["kimi", "glm", "codex", "deepseek"], easy: ["deepseek", "kimi", "glm", "codex"] };
|
|
86
|
+
const CREW_PREF = { hard: ["codex", "glm", "kimi", "deepseek", "openrouter"], medium: ["kimi", "glm", "codex", "deepseek", "openrouter"], easy: ["deepseek", "kimi", "glm", "codex", "openrouter"] };
|
|
74
87
|
|
|
75
88
|
export function advise(input, world = loadWorld()) {
|
|
76
89
|
const { profile, registry, caps, agents, scrooge } = world;
|
|
@@ -117,11 +130,15 @@ export function advise(input, world = loadWorld()) {
|
|
|
117
130
|
const m = registry.models?.["deepseek-v4-flash"] || { cost_in: 0.14, cost_out: 0.28 };
|
|
118
131
|
est = +((FORECAST[p.difficulty] * 0.85 * m.cost_in + FORECAST[p.difficulty] * 0.15 * m.cost_out) / 1e6).toFixed(2);
|
|
119
132
|
}
|
|
120
|
-
|
|
133
|
+
let why_r = p.difficulty === "hard"
|
|
121
134
|
? `hard → strongest available coder (${agent}); its ${pool} pool means ${pool === "api" ? "real $ but cheapest capable" : "$0 marginal on existing quota"}`
|
|
122
135
|
: p.difficulty === "medium"
|
|
123
136
|
? `medium → solid mid-tier (${agent}) keeps frontier seats free for hard work; ${pool === "api" ? "metered" : "quota"} pool`
|
|
124
137
|
: `easy → cheapest seat (${agent})`;
|
|
138
|
+
// OpenRouter live-select ranks capability×cost ACROSS the catalog once `scrooge-capabilities`
|
|
139
|
+
// has scored it (AA scores + price proxy + per-difficulty cost weighting → hard escalates to a
|
|
140
|
+
// strong model, easy stays cheap). If it hasn't been run, routing falls back to cost-only.
|
|
141
|
+
if (agent === "openrouter" && p.difficulty === "hard") why_r += ` — OpenRouter ranks capability×cost; run \`scrooge-capabilities\` to keep the catalog scored (or pin openrouter:openrouter/<vendor>/<model>)`;
|
|
125
142
|
return { ...p, executor: agent, pool, est_cost_usd: est, reason: why_r };
|
|
126
143
|
});
|
|
127
144
|
// crew-size rationale: seats are EMERGENT from the work, and we say so
|
package/bin/crew-runner.mjs
CHANGED
|
@@ -69,6 +69,12 @@ const CLI = {
|
|
|
69
69
|
next: `opencode run -c{M} "$(cat {P})"`, mflag: " -m ", env: join(homedir(), ".token-scrooge", ".env") },
|
|
70
70
|
opencode: { first: `opencode run{M} "$(cat {P})"`,
|
|
71
71
|
next: `opencode run -c{M} "$(cat {P})"`, mflag: " -m ", env: join(homedir(), ".token-scrooge", ".env") },
|
|
72
|
+
// OpenRouter rides the opencode CLI exactly like deepseek/glm, but under its OWN agent label so
|
|
73
|
+
// its bus identity is `openrouter:<project>` (RELAY_AGENT is set per-spawn) — never colliding with
|
|
74
|
+
// the glm `opencode` seat. Model ids come pre-qualified (`openrouter/<vendor>/<model>`). Sources
|
|
75
|
+
// the token-scrooge .env so an existing OPENROUTER_API_KEY authenticates with no extra wiring.
|
|
76
|
+
openrouter: { first: `opencode run{M} "$(cat {P})"`,
|
|
77
|
+
next: `opencode run -c{M} "$(cat {P})"`, mflag: " -m ", env: join(homedir(), ".token-scrooge", ".env") },
|
|
72
78
|
claude: { first: `claude{M} -p "$(cat {P})" --dangerously-skip-permissions`,
|
|
73
79
|
next: `claude -c{M} -p "$(cat {P})" --dangerously-skip-permissions`, mflag: " --model " },
|
|
74
80
|
};
|
package/bin/crew.sh
CHANGED
|
@@ -75,7 +75,7 @@ SCROOGE="$BUS_DIR/engine/bin/scrooge"
|
|
|
75
75
|
resolve_model() {
|
|
76
76
|
local agent="$1" provider="$2" task="$3" diff="$4" cands="" out=""
|
|
77
77
|
case "$agent" in
|
|
78
|
-
opencode|deepseek)
|
|
78
|
+
opencode|deepseek|openrouter)
|
|
79
79
|
cands="$(opencode models "$provider" 2>/dev/null | tr '\n' ' ')"
|
|
80
80
|
[ -n "$cands" ] || { echo "[crew] no live models via 'opencode models $provider' — CLI default" >&2; return 0; }
|
|
81
81
|
out="$(python3 "$SCROOGE" route --candidates "$cands" -t "$task" -d "$diff" --json 2>/dev/null)" ;;
|
|
@@ -133,6 +133,9 @@ spawn_grid() { # $@ = agents — (re)computes the grid for THIS batch and spawn
|
|
|
133
133
|
for SPEC in "$@"; do
|
|
134
134
|
AGENT="${SPEC%%:*}" # agent[:provider[/model]] — model rides in as CREW_MODEL
|
|
135
135
|
FIELD=""; [ "$SPEC" != "$AGENT" ] && FIELD="${SPEC#*:}"
|
|
136
|
+
# `trantor up openrouter` (no provider) → default the opencode provider to openrouter so it
|
|
137
|
+
# live-selects from the OpenRouter catalog instead of falling back to opencode's default model.
|
|
138
|
+
[ "$AGENT" = "openrouter" ] && [ -z "$FIELD" ] && FIELD="openrouter"
|
|
136
139
|
MODEL=""
|
|
137
140
|
if [ -n "$FIELD" ]; then
|
|
138
141
|
case "$FIELD" in
|
package/bin/doctor.mjs
CHANGED
|
@@ -61,6 +61,10 @@ const CLIS = [
|
|
|
61
61
|
{ name: "kimi", bin: "kimi", wired: () => !!read(join(H, ".kimi", "mcp.json"))?.mcpServers?.relay, auth: () => existsSync(join(H, ".kimi", "credentials")), login: "kimi → /login (Kimi account or Moonshot API key)" },
|
|
62
62
|
{ name: "deepseek (via opencode)", bin: "opencode", wired: () => !!read(join(H, ".config", "opencode", "opencode.json"))?.mcp?.relay, auth: () => !!process.env.DEEPSEEK_API_KEY || (existsSync(join(H, ".agent-bus", ".env")) && readFileSync(join(H, ".agent-bus", ".env"), "utf8").includes("DEEPSEEK_API_KEY")) || !!read(join(H, ".local", "share", "opencode", "auth.json")), login: `get a key at platform.deepseek.com, then: echo 'DEEPSEEK_API_KEY=sk-…' >> ~/.agent-bus/.env` },
|
|
63
63
|
{ name: "glm (via opencode · coding plan)", bin: "opencode", wired: () => !!read(join(H, ".config", "opencode", "opencode.json"))?.mcp?.relay, auth: () => !!read(join(H, ".config", "opencode", "opencode.json"))?.provider?.["zai-coding-plan"]?.options?.apiKey, login: `put your Z.ai coding-plan key at ~/.config/opencode/opencode.json → provider["zai-coding-plan"].options.apiKey, then seat: trantor up opencode:zai-coding-plan/glm-5.1` },
|
|
64
|
+
// OpenRouter — the BYOM on-ramp: ONE key fronts hundreds of models. Rides opencode; the same
|
|
65
|
+
// OPENROUTER_API_KEY Scrooge already uses authenticates the crew seat (the runner sources the
|
|
66
|
+
// .env files). Available the moment the key exists in env/opencode + declared `openrouter=api`.
|
|
67
|
+
{ name: "openrouter (via opencode · BYOM, hundreds of models)", bin: "opencode", wired: () => !!read(join(H, ".config", "opencode", "opencode.json"))?.mcp?.relay, auth: () => !!process.env.OPENROUTER_API_KEY || !!read(join(H, ".config", "opencode", "opencode.json"))?.provider?.openrouter?.options?.apiKey || [join(H, ".token-scrooge", ".env"), join(H, ".agent-bus", ".env")].some(f => { try { return readFileSync(f, "utf8").includes("OPENROUTER_API_KEY"); } catch { return false; } }), login: `get a key at openrouter.ai/keys, then: echo 'OPENROUTER_API_KEY=sk-or-…' >> ~/.agent-bus/.env && trantor profile set openrouter=api && scrooge-capabilities (scores the catalog so the crew routes it by difficulty). Seat: trantor up openrouter (live-selects) or pin trantor up openrouter:openrouter/<vendor>/<model>` },
|
|
64
68
|
];
|
|
65
69
|
let installed = 0;
|
|
66
70
|
for (const c of CLIS) {
|
package/engine/bin/scrooge
CHANGED
|
@@ -171,8 +171,11 @@ def model_quality(caps, mid, metric):
|
|
|
171
171
|
v = c.get("intelligence") # fall back to the general index
|
|
172
172
|
return float(v) if isinstance(v, (int, float)) else 0.0
|
|
173
173
|
|
|
174
|
-
def blended_cost(reg, mid):
|
|
175
|
-
|
|
174
|
+
def blended_cost(reg, caps, mid):
|
|
175
|
+
# The curated registry carries cost for its own models; for CATALOG models (OpenRouter's
|
|
176
|
+
# hundreds, brought via --candidates) the per-model price rides on the capability entry
|
|
177
|
+
# instead — so a brought model is ranked on its real price without bloating the registry.
|
|
178
|
+
m = reg["models"].get(mid) or (caps.get(mid) if isinstance(caps, dict) else None) or {}
|
|
176
179
|
return max(1e-6, 0.3 * m.get("cost_in", 0) + 0.7 * m.get("cost_out", 0))
|
|
177
180
|
|
|
178
181
|
def weigh_candidates(reg, caps, cand_ids, task, difficulty):
|
|
@@ -187,8 +190,16 @@ def weigh_candidates(reg, caps, cand_ids, task, difficulty):
|
|
|
187
190
|
floor = scored_q[min(len(scored_q) - 1, int(round(pct * (len(scored_q) - 1))))]
|
|
188
191
|
survivors = [(mid, q) for mid, q in quals if q >= floor] or quals
|
|
189
192
|
rw = reg.get("routing") or {}
|
|
190
|
-
qw
|
|
191
|
-
|
|
193
|
+
qw = rw.get("q_weight", 1.5)
|
|
194
|
+
# cost-weight scales DOWN as difficulty rises: EASY work optimizes price (cheap wins), HARD
|
|
195
|
+
# work prioritizes capability so it can escalate to a genuinely strong model instead of the
|
|
196
|
+
# cheapest-that-clears-the-floor (the "deepseek-flash wins everything" trap on a huge catalog).
|
|
197
|
+
# It still weighs cost (hard picks a strong *value* model, not a blind frontier overpay).
|
|
198
|
+
# Per-difficulty override via registry.routing.c_weight_<difficulty>; legacy c_weight still honored.
|
|
199
|
+
CW_BY_DIFF = {"easy": 0.65, "medium": 0.5, "hard": 0.1}
|
|
200
|
+
cw_key = "c_weight_" + str(difficulty)
|
|
201
|
+
cw = rw[cw_key] if cw_key in rw else (CW_BY_DIFF[difficulty] if difficulty in CW_BY_DIFF else rw.get("c_weight", 0.5))
|
|
202
|
+
out = [(mid, (max(q, 1e-6) ** qw) / (blended_cost(reg, caps, mid) ** cw)) for mid, q in survivors]
|
|
192
203
|
out.sort(key=lambda x: -x[1])
|
|
193
204
|
return out
|
|
194
205
|
|
|
@@ -22,7 +22,7 @@ Usage: scrooge-capabilities # refresh from AA (+OpenRouter), show a
|
|
|
22
22
|
scrooge-capabilities --dry-run # fetch + match, print, but don't write
|
|
23
23
|
Exit: 0 updated · 1 nothing fetched (no key / network) · 2 error.
|
|
24
24
|
"""
|
|
25
|
-
import sys, os, json, argparse, urllib.request, urllib.error
|
|
25
|
+
import sys, os, json, argparse, math, urllib.request, urllib.error
|
|
26
26
|
|
|
27
27
|
HOME = os.path.expanduser("~")
|
|
28
28
|
SCROOGE_DIR = os.environ.get("SCROOGE_HOME", os.path.join(HOME, ".token-scrooge"))
|
|
@@ -122,13 +122,35 @@ def fetch_openrouter():
|
|
|
122
122
|
if not mid:
|
|
123
123
|
continue
|
|
124
124
|
arch = m.get("architecture") or {}
|
|
125
|
-
|
|
125
|
+
pr = m.get("pricing") or {}
|
|
126
|
+
def _per_m(v): # OpenRouter prices are USD PER TOKEN → $/1M tokens
|
|
127
|
+
try:
|
|
128
|
+
return round(float(v) * 1e6, 4)
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
# KEY BY THE RAW LAST SEGMENT (dots intact) — this is exactly what `scrooge route` uses to
|
|
132
|
+
# look a candidate up (by_bare = id.split("/")[-1], no normalisation), so the keys must match.
|
|
133
|
+
out[mid.split("/")[-1]] = {
|
|
134
|
+
"full": mid,
|
|
126
135
|
"context": m.get("context_length"),
|
|
127
136
|
"modalities": arch.get("input_modalities"),
|
|
137
|
+
"cost_in": _per_m(pr.get("prompt")),
|
|
138
|
+
"cost_out": _per_m(pr.get("completion")),
|
|
128
139
|
}
|
|
129
|
-
sys.stderr.write(DIM(" OpenRouter: %d models fetched (context/modality).\n" % len(out)))
|
|
140
|
+
sys.stderr.write(DIM(" OpenRouter: %d models fetched (pricing/context/modality).\n" % len(out)))
|
|
130
141
|
return out
|
|
131
142
|
|
|
143
|
+
def price_proxy_capability(cost_out):
|
|
144
|
+
"""A transparent fallback capability when AA has no score for a brought model: price is a
|
|
145
|
+
decent proxy for tier (frontier models cost more). Monotonic in cost_out ($/1M), with an
|
|
146
|
+
HONEST ceiling — never claim frontier capability from price alone. Gives every catalog model
|
|
147
|
+
a non-zero, rank-able score so the difficulty floor can separate hard from easy work; a later
|
|
148
|
+
AA refresh upgrades it to a real score."""
|
|
149
|
+
if not isinstance(cost_out, (int, float)) or cost_out <= 0:
|
|
150
|
+
return 8.0 # free / unknown price → low floor
|
|
151
|
+
cap = 30.0 + 20.0 * math.log10(cost_out + 0.1)
|
|
152
|
+
return round(max(5.0, min(72.0, cap)), 1)
|
|
153
|
+
|
|
132
154
|
def aa_scores(m):
|
|
133
155
|
ev = m.get("evaluations") or {}
|
|
134
156
|
g = ev.get("gpqa")
|
|
@@ -162,6 +184,7 @@ def main():
|
|
|
162
184
|
return 1
|
|
163
185
|
|
|
164
186
|
today = __import__("time").strftime("%Y-%m-%d")
|
|
187
|
+
by_or_norm = {norm(k): v for k, v in by_or.items()} # by_or is raw-keyed; registry ids are dash-form
|
|
165
188
|
matched, unmatched = [], []
|
|
166
189
|
for mid in reg["models"]:
|
|
167
190
|
existing = caps.get(mid) if isinstance(caps.get(mid), dict) else {}
|
|
@@ -175,7 +198,7 @@ def main():
|
|
|
175
198
|
matched.append(mid)
|
|
176
199
|
elif by_slug:
|
|
177
200
|
unmatched.append(mid)
|
|
178
|
-
orx =
|
|
201
|
+
orx = by_or_norm.get(norm(mid))
|
|
179
202
|
if orx:
|
|
180
203
|
if orx.get("context"):
|
|
181
204
|
rec["context"] = orx["context"]
|
|
@@ -184,10 +207,53 @@ def main():
|
|
|
184
207
|
if rec:
|
|
185
208
|
caps[mid] = rec
|
|
186
209
|
|
|
210
|
+
# ---- OpenRouter CATALOG ingestion: make every brought model routable by difficulty -------
|
|
211
|
+
# The crew passes OpenRouter's hundreds of models as candidates; the router ranks by
|
|
212
|
+
# capability (gated by a difficulty floor) ÷ price. Write each catalog model as a first-class
|
|
213
|
+
# entry keyed by its RAW bare name (matching `scrooge route`'s lookup): a REAL AA score when
|
|
214
|
+
# the model matches a slug, else a transparent price-tier PROXY — plus its real price so cost
|
|
215
|
+
# ranking works. Registry-curated models are never shadowed. Marked `_src` for honesty; a
|
|
216
|
+
# later AA refresh upgrades proxies in place.
|
|
217
|
+
or_real = or_proxy = 0
|
|
218
|
+
for bare, info in by_or.items():
|
|
219
|
+
if bare in reg["models"]: # curated registry model — leave it authoritative
|
|
220
|
+
continue
|
|
221
|
+
existing = caps.get(bare) if isinstance(caps.get(bare), dict) else {}
|
|
222
|
+
rec = dict(existing)
|
|
223
|
+
if info.get("cost_in") is not None:
|
|
224
|
+
rec["cost_in"] = info["cost_in"]
|
|
225
|
+
if info.get("cost_out") is not None:
|
|
226
|
+
rec["cost_out"] = info["cost_out"]
|
|
227
|
+
if info.get("context"):
|
|
228
|
+
rec.setdefault("context", info["context"])
|
|
229
|
+
has_real = isinstance(rec.get("coding"), (int, float)) or isinstance(rec.get("intelligence"), (int, float))
|
|
230
|
+
am = None
|
|
231
|
+
if by_slug:
|
|
232
|
+
full = info.get("full", "")
|
|
233
|
+
am = by_slug.get(norm(full)) or by_slug.get(norm(full.split("/")[-1])) or by_slug.get(bare)
|
|
234
|
+
if am:
|
|
235
|
+
rec.update({k: v for k, v in aa_scores(am).items() if v is not None})
|
|
236
|
+
rec["updated"] = today
|
|
237
|
+
rec["source"] = "artificialanalysis"
|
|
238
|
+
rec.pop("_src", None)
|
|
239
|
+
or_real += 1
|
|
240
|
+
elif not has_real or rec.get("_src") == "openrouter-price-proxy":
|
|
241
|
+
p = price_proxy_capability(rec.get("cost_out"))
|
|
242
|
+
if p is not None:
|
|
243
|
+
rec["coding"] = rec["intelligence"] = rec["reasoning"] = p
|
|
244
|
+
rec["_src"] = "openrouter-price-proxy"
|
|
245
|
+
rec["updated"] = today
|
|
246
|
+
or_proxy += 1
|
|
247
|
+
if rec:
|
|
248
|
+
caps[bare] = rec
|
|
249
|
+
|
|
187
250
|
caps["_meta"].update({"source": "artificialanalysis.ai + openrouter", "refreshed": today,
|
|
188
|
-
"attribution": "https://artificialanalysis.ai/"
|
|
251
|
+
"attribution": "https://artificialanalysis.ai/",
|
|
252
|
+
"openrouter_catalog": {"aa_scored": or_real, "price_proxy": or_proxy}})
|
|
189
253
|
|
|
190
254
|
sys.stderr.write(GOLD("🪙 scrooge-capabilities — %d matched, %d unmatched\n" % (len(matched), len(unmatched))))
|
|
255
|
+
if by_or:
|
|
256
|
+
sys.stderr.write(GOLD(" OpenRouter catalog — %d AA-scored, %d price-proxy (now routable by difficulty)\n" % (or_real, or_proxy)))
|
|
191
257
|
for mid in matched:
|
|
192
258
|
c = caps[mid]
|
|
193
259
|
sys.stderr.write(" %s %-24s intel=%-5s code=%-5s reason=%-5s %st/s\n" % (
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression test for capability×cost routing — difficulty-aware escalation + catalog cost.
|
|
3
|
+
|
|
4
|
+
Runs the real weigh_candidates/blended_cost out of bin/scrooge against SYNTHETIC capability
|
|
5
|
+
data (no network, no key), asserting:
|
|
6
|
+
1. catalog cost rides on the capability entry (blended_cost falls back to caps when the
|
|
7
|
+
registry doesn't have the model) — the OpenRouter-routing fix,
|
|
8
|
+
2. cost-weight is difficulty-aware: a cheap-but-decent model wins EASY, while HARD escalates
|
|
9
|
+
to a genuinely stronger model instead of the cheapest-that-clears-the-floor.
|
|
10
|
+
|
|
11
|
+
Exit 0 = all pass. Used to verify the T2 OpenRouter-routes-by-difficulty change.
|
|
12
|
+
"""
|
|
13
|
+
import os, sys
|
|
14
|
+
|
|
15
|
+
HERE = os.path.dirname(os.path.realpath(__file__))
|
|
16
|
+
SCROOGE = os.path.join(HERE, "bin", "scrooge")
|
|
17
|
+
g = {"__name__": "scr", "__file__": SCROOGE}
|
|
18
|
+
exec(compile(open(SCROOGE).read(), "scrooge", "exec"), g)
|
|
19
|
+
|
|
20
|
+
# Synthetic registry has NO catalog models — cost must come from caps (the OpenRouter case).
|
|
21
|
+
reg = {"models": {}, "routing": {}}
|
|
22
|
+
caps = {
|
|
23
|
+
"cheap-weak": {"coding": 20, "cost_in": 0.05, "cost_out": 0.10}, # junk-tier
|
|
24
|
+
"cheap-strong": {"coding": 56, "cost_in": 0.14, "cost_out": 0.28}, # deepseek-flash-like
|
|
25
|
+
"mid-strong": {"coding": 69, "cost_in": 0.60, "cost_out": 2.40}, # glm-5.2-like
|
|
26
|
+
"frontier": {"coding": 75, "cost_in": 5.00, "cost_out": 22.50}, # gpt-5.5-like
|
|
27
|
+
}
|
|
28
|
+
cands = list(caps.keys())
|
|
29
|
+
|
|
30
|
+
fails = []
|
|
31
|
+
def ok(name, cond):
|
|
32
|
+
print((" ✓ " if cond else " ✗ ") + name)
|
|
33
|
+
if not cond:
|
|
34
|
+
fails.append(name)
|
|
35
|
+
|
|
36
|
+
# 1. catalog cost via caps fallback (model absent from registry)
|
|
37
|
+
ok("blended_cost falls back to the capability entry for catalog models",
|
|
38
|
+
abs(g["blended_cost"](reg, caps, "frontier") - (0.3 * 5.0 + 0.7 * 22.5)) < 1e-6)
|
|
39
|
+
ok("blended_cost is 1e-6 for an entirely unknown model (no crash)",
|
|
40
|
+
g["blended_cost"](reg, caps, "does-not-exist") == 1e-6)
|
|
41
|
+
|
|
42
|
+
def winner(diff):
|
|
43
|
+
return g["weigh_candidates"](reg, caps, cands, "code", diff)[0][0]
|
|
44
|
+
|
|
45
|
+
easy, medium, hard = winner("easy"), winner("medium"), winner("hard")
|
|
46
|
+
print(" picks → easy=%s medium=%s hard=%s" % (easy, medium, hard))
|
|
47
|
+
|
|
48
|
+
# 2. difficulty-aware escalation
|
|
49
|
+
ok("easy prefers a cheap model (cost-optimized)", caps[easy]["coding"] <= caps["mid-strong"]["coding"])
|
|
50
|
+
ok("hard escalates to a stronger model than easy", caps[hard]["coding"] > caps[easy]["coding"])
|
|
51
|
+
ok("hard reaches genuine strength (>= mid-strong tier)", caps[hard]["coding"] >= caps["mid-strong"]["coding"])
|
|
52
|
+
ok("the junk-tier model never wins any difficulty", "cheap-weak" not in (easy, medium, hard))
|
|
53
|
+
|
|
54
|
+
print(("\nALL PASS" if not fails else "\nFAILED: %d" % len(fails)))
|
|
55
|
+
sys.exit(1 if fails else 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trantor": "bin/cli.mjs"
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
"zod": "^4.4.3"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-focus.mjs"
|
|
13
|
+
"test": "node test.mjs && node test-scenarios.mjs && node test-failure.mjs && node test-handoff.mjs && node test-agents.mjs && node test-update.mjs && node test-handoff-guard.mjs && node test-balances.mjs && node test-subagent-cost.mjs && node test-inbox.mjs && node test-inflight.mjs && node test-focus.mjs && python3 engine/test-routing.py"
|
|
14
14
|
},
|
|
15
|
-
"description": "The hub-world for AI agent crews — orchestrate Claude Code, Codex,
|
|
15
|
+
"description": "The hub-world for AI agent crews — orchestrate Claude Code, Codex, GLM, Kimi, DeepSeek & any OpenRouter model as live crews with a plan-aware Advisor, a Kanban/flow command center, a testing gate, and an economics brain (Scrooge).",
|
|
16
16
|
"files": [
|
|
17
17
|
"hub.mjs",
|
|
18
18
|
"mcp.mjs",
|
package/skills/crew/SKILL.md
CHANGED
|
@@ -50,9 +50,15 @@ EXACT launch spec per provider (do not improvise these):
|
|
|
50
50
|
| Kimi | `kimi` | Moonshot coding-plan |
|
|
51
51
|
| DeepSeek | `deepseek:deepseek` | runs via opencode; `deepseek` alone = CLI default |
|
|
52
52
|
| **GLM (Z.ai)** | **`opencode:zai-coding-plan`** | **runs via opencode, NOT a bare `glm`/`zai` terminal command.** `opencode:zai-coding-plan/glm-5.2` pins a model; `opencode:zai-coding-plan` live-selects. |
|
|
53
|
+
| **OpenRouter (BYOM)** | **`openrouter`** | **the bring-your-own-model on-ramp — one key fronts hundreds of vendors (incl. ones with no CLI).** Bare `openrouter` live-selects the best OpenRouter model for the difficulty; pin one with `openrouter:openrouter/<vendor>/<model>`. Its own bus identity `openrouter:<project>` (never collides with the GLM `opencode` seat). |
|
|
53
54
|
|
|
54
55
|
`agent:provider` live-selects the best model now; `agent:provider/model` pins one. Example:
|
|
55
56
|
`trantor up codex kimi deepseek:deepseek opencode:zai-coding-plan --task code --difficulty hard`.
|
|
57
|
+
**Whatever the advisor's `launch` field says, run that verbatim** — the roster above is the menu,
|
|
58
|
+
but the advisor already picked the right seats and specs for THIS work (a user who's only brought
|
|
59
|
+
an OpenRouter key, for instance, gets `openrouter` for everything; one who brought five providers
|
|
60
|
+
gets a load-balanced spread). New providers a user brings (OpenRouter today; any opencode-supported
|
|
61
|
+
vendor next) appear automatically once declared via `trantor profile set <name>=api` + a key.
|
|
56
62
|
|
|
57
63
|
⚠️ **Gemini CLI is RETIRED (Google killed the free seat 2026-06-18).** The advisor no longer
|
|
58
64
|
offers it and you must NOT fire up `gemini` — `gemini --yolo` exits 1 and crash-loops on the
|